From 7240f6efa763b1844b3b03cfe4bee648b9bbab99 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 5 Jun 2023 09:25:02 +0200 Subject: [PATCH] Add docker, prisma and worker POC Add docker-compose with Postgres Install Prisma and generate empty schema Install Prisma client Add app config model and migration Add repository for Algolia Configuration Migrate metadata to postgres Replace webhooks metadata with PRisma Add worker and skeleton code Implement worker job and removed from the frontend Attempt to display jobs list Worker utils Run worker in the same thread on dev Run worker in the same thread on dev Build scripts fix dev mode Dockerfiles prod dockerfiles docker wip docker wip wip working docker wip working docker wip - working without prisma migrate --- .dockerignore | 2 + apps/search/.dockerignore | 2 + apps/search/.env.example | 8 +- apps/search/.gitignore | 1 + apps/search/app.prod.Dockerfile | 71 +++ apps/search/next.config.js | 4 + apps/search/package.json | 29 +- .../20230605074219_init/migration.sql | 13 + .../prisma/migrations/migration_lock.toml | 3 + apps/search/prisma/schema.prisma | 19 + apps/search/public/.gitkeep | 0 apps/search/saleor-app.ts | 9 - .../components/ImportProductsToAlgolia.tsx | 82 +--- .../src/components/useQueryAllProducts.tsx | 3 + apps/search/src/db/prisma.ts | 9 + .../AlgoliaConfigurationRepository.ts | 42 ++ apps/search/src/instrumentation.ts | 17 + .../lib/algolia/getAlgoliaConfiguration.ts | 62 +-- apps/search/src/lib/metadata.ts | 84 ---- apps/search/src/pages/api/configuration.ts | 66 +-- apps/search/src/pages/api/index-products.ts | 18 + apps/search/src/pages/api/jobs.ts | 10 + apps/search/src/pages/api/webhooks-status.ts | 34 +- .../api/webhooks/saleor/product_created.ts | 6 +- .../api/webhooks/saleor/product_deleted.ts | 6 +- .../api/webhooks/saleor/product_updated.ts | 6 +- .../saleor/product_variant_created.ts | 6 +- .../saleor/product_variant_deleted.ts | 6 +- .../saleor/product_variant_updated.ts | 6 +- .../get-products-and-send-to-algolia.ts | 93 ++++ .../index-saleor-products.ts | 42 ++ apps/search/src/worker/runner.ts | 30 ++ apps/search/src/worker/worker-utils.ts | 18 + apps/search/tsconfig.json | 2 +- apps/search/turbo.json | 5 +- docker-compose.db.yml | 23 + docker-compose.search.prod.yml | 57 +++ pnpm-lock.yaml | 446 +++++++++++++++++- turbo.json | 5 + 39 files changed, 1044 insertions(+), 301 deletions(-) create mode 100644 .dockerignore create mode 100644 apps/search/.dockerignore create mode 100644 apps/search/.gitignore create mode 100644 apps/search/app.prod.Dockerfile create mode 100644 apps/search/prisma/migrations/20230605074219_init/migration.sql create mode 100644 apps/search/prisma/migrations/migration_lock.toml create mode 100644 apps/search/prisma/schema.prisma create mode 100644 apps/search/public/.gitkeep create mode 100644 apps/search/src/db/prisma.ts create mode 100644 apps/search/src/domain/algolia-configuration/AlgoliaConfigurationRepository.ts create mode 100644 apps/search/src/instrumentation.ts delete mode 100644 apps/search/src/lib/metadata.ts create mode 100644 apps/search/src/pages/api/index-products.ts create mode 100644 apps/search/src/pages/api/jobs.ts create mode 100644 apps/search/src/worker/index-saleor-products/get-products-and-send-to-algolia.ts create mode 100644 apps/search/src/worker/index-saleor-products/index-saleor-products.ts create mode 100644 apps/search/src/worker/runner.ts create mode 100644 apps/search/src/worker/worker-utils.ts create mode 100644 docker-compose.db.yml create mode 100644 docker-compose.search.prod.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1883a56 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules/ +**/.env \ No newline at end of file diff --git a/apps/search/.dockerignore b/apps/search/.dockerignore new file mode 100644 index 0000000..1883a56 --- /dev/null +++ b/apps/search/.dockerignore @@ -0,0 +1,2 @@ +node_modules/ +**/.env \ No newline at end of file diff --git a/apps/search/.env.example b/apps/search/.env.example index cbee4ca..6cd95e2 100644 --- a/apps/search/.env.example +++ b/apps/search/.env.example @@ -1,4 +1,10 @@ # Encryption key used by the EncryptedSettingsManager. Required by the production builds SECRET_KEY= -APP_LOG_LEVEL=info \ No newline at end of file +APP_LOG_LEVEL=info + +DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DATABASE + +# Conditionally run worker with Next use instrumentation hook (src/instrmentation.ts) +# This is handy for development but in production it should be a separate process +RUN_WORKER_IN_NEXT_PROCESS=false \ No newline at end of file diff --git a/apps/search/.gitignore b/apps/search/.gitignore new file mode 100644 index 0000000..ac97fc9 --- /dev/null +++ b/apps/search/.gitignore @@ -0,0 +1 @@ +worker-dist/ \ No newline at end of file diff --git a/apps/search/app.prod.Dockerfile b/apps/search/app.prod.Dockerfile new file mode 100644 index 0000000..218e4bc --- /dev/null +++ b/apps/search/app.prod.Dockerfile @@ -0,0 +1,71 @@ +# Source +# https://turbo.build/repo/docs/handbook/deploying-with-docker#example +# TODO https://pnpm.io/cli/fetch + +FROM node:18 AS base + +FROM base AS builder +#RUN apk add --no-cache libc6-compat +#RUN apk update + +# Set working directory +WORKDIR /app + +RUN yarn global add turbo@1.9.1 +RUN yarn global add pnpm@8.2.0 + +# Copy entire monorepo +COPY . . + +RUN turbo prune --scope="saleor-app-search" --docker + +# Add lockfile and package.json's of isolated subworkspace +FROM base AS installer +#RUN apk add --no-cache libc6-compat +#RUN apk update +WORKDIR /app +RUN yarn global add pnpm@8.2.0 + +ARG DATABASE_URL +ENV DATABASE_URL=${DATABASE_URL} + +# First install the dependencies (as they change less often) +COPY .gitignore .gitignore +COPY --from=builder /app/out/full/ . +#COPY --from=builder /app/out/json/ . +COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml +COPY --from=builder /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml + +RUN pnpm install --frozen-lockfile + +# Build the project +#COPY --from=builder /app/out/full/ . +COPY turbo.json turbo.json + +RUN pnpm turbo run build:app --filter="saleor-app-search" + +FROM base AS runner +WORKDIR /app + +ARG DATABASE_URL +ENV DATABASE_URL=${DATABASE_URL} + +# Don't run production as root +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs +USER nextjs + +COPY --from=installer /app/apps/search/next.config.js . +COPY --from=installer /app/apps/search/package.json . + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=installer --chown=nextjs:nodejs /app/apps/search/.next/standalone ./ +COPY --from=installer --chown=nextjs:nodejs /app/apps/search/.next/static ./apps/search/.next/static +COPY --from=installer --chown=nextjs:nodejs /app/apps/search/public ./apps/search/public +COPY --from=installer --chown=nextjs:nodejs /app/apps/search/prisma ./apps/search/prisma + + +CMD ["node", "apps/search/server.js"] + +# TODO Another entrypoint for worker \ No newline at end of file diff --git a/apps/search/next.config.js b/apps/search/next.config.js index 0dafa2f..59a27e1 100644 --- a/apps/search/next.config.js +++ b/apps/search/next.config.js @@ -15,6 +15,9 @@ const isSentryPropertiesInEnvironment = */ const moduleExports = { reactStrictMode: true, + experimental: { + instrumentationHook: true + }, images: { remotePatterns: [ { @@ -37,6 +40,7 @@ const moduleExports = { disableServerWebpackPlugin: !isSentryPropertiesInEnvironment, disableClientWebpackPlugin: !isSentryPropertiesInEnvironment, }, + output: "standalone", }; const sentryWebpackPluginOptions = { diff --git a/apps/search/package.json b/apps/search/package.json index 8d669b1..5f73030 100644 --- a/apps/search/package.json +++ b/apps/search/package.json @@ -2,17 +2,25 @@ "name": "saleor-app-search", "version": "1.9.3", "scripts": { - "build": "pnpm generate && next build", - "dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev", - "fetch-schema": "curl https://raw.githubusercontent.com/saleor/saleor/${npm_package_saleor_schemaVersion}/saleor/graphql/schema.graphql > graphql/schema.graphql", - "generate": "graphql-codegen", + "dev:app": "pnpm generate && NODE_OPTIONS='--inspect' next dev", + "build:app": "pnpm generate && next build", + "start:app": "next start", + "build": "concurrently \"pnpm build:app\" \"pnpm build:worker\"", + "dev": "concurrently \"pnpm dev:app\" \"pnpm dev:worker\"", "lint": "next lint", "lint:fix": "eslint --fix .", - "start": "next start", - "test": "vitest" + "fetch-schema": "curl https://raw.githubusercontent.com/saleor/saleor/${npm_package_saleor_schemaVersion}/saleor/graphql/schema.graphql > graphql/schema.graphql", + "generate": "graphql-codegen && npm run prisma:generate", + "test": "vitest", + "dev:worker": "pnpm generate && tsx src/worker/runner.ts --watch", + "build:worker": "pnpm generate && tsup src/worker/runner.ts --outDir worker-dist", + "start:worker": "node worker-dist/runner.js", + "prisma:generate": "prisma generate" }, "dependencies": { + "graphile-worker": "^0.13.0", "@hookform/resolvers": "^3.1.0", + "@prisma/client": "^4.15.0", "@saleor/app-sdk": "0.39.1", "@saleor/apps-shared": "workspace:*", "@saleor/apps-ui": "workspace:*", @@ -36,7 +44,9 @@ "react-hook-form": "^7.43.9", "react-query": "^3.39.3", "urql": "^3.0.3", - "zod": "^3.21.4" + "zod": "^3.21.4", + "dotenv": "^16.1.4", + "prisma": "^4.15.0" }, "devDependencies": { "@graphql-codegen/cli": "3.2.2", @@ -56,7 +66,10 @@ "node-mocks-http": "^1.12.2", "typescript": "5.1.3", "vite": "4.3.9", - "vitest": "0.31.3" + "vitest": "0.31.3", + "tsup": "^6.7.0", + "concurrently": "^8.1.0", + "tsx": "^3.12.7" }, "private": true, "saleor": { diff --git a/apps/search/prisma/migrations/20230605074219_init/migration.sql b/apps/search/prisma/migrations/20230605074219_init/migration.sql new file mode 100644 index 0000000..939bdfc --- /dev/null +++ b/apps/search/prisma/migrations/20230605074219_init/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "AlgoliaConfiguration" ( + "id" SERIAL NOT NULL, + "appId" TEXT NOT NULL, + "indexNamePrefix" TEXT, + "secretKey" TEXT NOT NULL, + "saleorApiUrl" TEXT NOT NULL, + + CONSTRAINT "AlgoliaConfiguration_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "AlgoliaConfiguration_saleorApiUrl_key" ON "AlgoliaConfiguration"("saleorApiUrl"); diff --git a/apps/search/prisma/migrations/migration_lock.toml b/apps/search/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/apps/search/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/apps/search/prisma/schema.prisma b/apps/search/prisma/schema.prisma new file mode 100644 index 0000000..a5225f8 --- /dev/null +++ b/apps/search/prisma/schema.prisma @@ -0,0 +1,19 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model AlgoliaConfiguration { + id Int @id @default(autoincrement()) + appId String + indexNamePrefix String? + secretKey String // TODO encryption + saleorApiUrl String @unique // Reference - maybe it can be unique id? This will share config where 2 apps installed +} diff --git a/apps/search/public/.gitkeep b/apps/search/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/search/saleor-app.ts b/apps/search/saleor-app.ts index 59689fc..6cb13ac 100644 --- a/apps/search/saleor-app.ts +++ b/apps/search/saleor-app.ts @@ -37,15 +37,6 @@ switch (aplType) { } } -if (!process.env.SECRET_KEY && process.env.NODE_ENV === "production") { - throw new Error( - "For production deployment SECRET_KEY is mandatory to use EncryptedSettingsManager." - ); -} - -// Use placeholder value for the development -export const settingsManagerSecretKey = process.env.SECRET_KEY || "CHANGE_ME"; - export const saleorApp = new SaleorApp({ apl, }); diff --git a/apps/search/src/components/ImportProductsToAlgolia.tsx b/apps/search/src/components/ImportProductsToAlgolia.tsx index 44a54dd..9844f19 100644 --- a/apps/search/src/components/ImportProductsToAlgolia.tsx +++ b/apps/search/src/components/ImportProductsToAlgolia.tsx @@ -1,19 +1,14 @@ import { Box, Button, Text } from "@saleor/macaw-ui/next"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { AlgoliaSearchProvider } from "../lib/algolia/algoliaSearchProvider"; import { useConfiguration } from "../lib/configuration"; -import { Products, useQueryAllProducts } from "./useQueryAllProducts"; -import { useWebhooksStatus } from "../lib/useWebhooksStatus"; - -const BATCH_SIZE = 100; +import { Products } from "./useQueryAllProducts"; +import { useAuthenticatedFetch } from "@saleor/app-sdk/app-bridge"; export const ImportProductsToAlgolia = () => { - const [algoliaConfigured, setAlgoliaConfigured] = useState(null); - const [started, setStarted] = useState(false); - const [currentProductIndex, setCurrentProductIndex] = useState(0); - const [isAlgoliaImporting, setIsAlgoliaImporting] = useState(false); + const fetch = useAuthenticatedFetch(); - const products = useQueryAllProducts(!started); + const [algoliaConfigured, setAlgoliaConfigured] = useState(null); const algoliaConfiguration = useConfiguration(); @@ -32,10 +27,6 @@ export const ImportProductsToAlgolia = () => { algoliaConfiguration?.data?.secretKey, ]); - const importProducts = useCallback(() => { - setStarted(true); - }, []); - useEffect(() => { if (searchProvider) { searchProvider @@ -45,39 +36,29 @@ export const ImportProductsToAlgolia = () => { } }, [searchProvider]); - useEffect(() => { - if (!searchProvider || isAlgoliaImporting || products.length <= currentProductIndex) { - return; - } - (async () => { - setIsAlgoliaImporting(true); - const productsBatchStartIndex = currentProductIndex; - const productsBatchEndIndex = Math.min(currentProductIndex + BATCH_SIZE, products.length); - const productsBatch = products.slice(productsBatchStartIndex, productsBatchEndIndex); - - await searchProvider.updatedBatchProducts(productsBatch); - - setIsAlgoliaImporting(false); - setCurrentProductIndex(productsBatchEndIndex); - })(); - }, [searchProvider, currentProductIndex, isAlgoliaImporting, products]); - return ( - + {searchProvider && algoliaConfigured ? ( Importing products & variants - Trigger initial indexing for products catalogue. It can take few minutes.{" "} + Trigger initial indexing for products catalogue. It can take few minutes and will run in + the background - - Do not close the app - its running client-side - - - + @@ -89,29 +70,6 @@ export const ImportProductsToAlgolia = () => { Configure Algolia first )} - - {started && ( -
- {countVariants(products, currentProductIndex)} /{" "} - {countVariants(products, products.length)} - -
- )}
); }; diff --git a/apps/search/src/components/useQueryAllProducts.tsx b/apps/search/src/components/useQueryAllProducts.tsx index e733601..340a9b1 100644 --- a/apps/search/src/components/useQueryAllProducts.tsx +++ b/apps/search/src/components/useQueryAllProducts.tsx @@ -13,6 +13,9 @@ export type Products = NonNullable< ProductsDataForImportQuery["products"] >["edges"][number]["node"][]; +/** + * @deprecated + */ export const useQueryAllProducts = (paused: boolean) => { const { appBridgeState } = useAppBridge(); const saleorApiUrl = appBridgeState?.saleorApiUrl!; diff --git a/apps/search/src/db/prisma.ts b/apps/search/src/db/prisma.ts new file mode 100644 index 0000000..92e30a0 --- /dev/null +++ b/apps/search/src/db/prisma.ts @@ -0,0 +1,9 @@ +import { PrismaClient } from "@prisma/client"; + +export const prisma = new PrismaClient(); +export type Prisma = typeof prisma; + +// todo verify if this is enough cleanup +process.on("exit", () => { + prisma.$disconnect(); +}); diff --git a/apps/search/src/domain/algolia-configuration/AlgoliaConfigurationRepository.ts b/apps/search/src/domain/algolia-configuration/AlgoliaConfigurationRepository.ts new file mode 100644 index 0000000..6237e66 --- /dev/null +++ b/apps/search/src/domain/algolia-configuration/AlgoliaConfigurationRepository.ts @@ -0,0 +1,42 @@ +import { prisma, Prisma } from "../../db/prisma"; + +export class AlgoliaConfigurationRepository { + constructor(private prisma: Prisma) {} + + getConfiguration(saleorApiUrl: string) { + return this.prisma.algoliaConfiguration.findFirst({ + where: { + saleorApiUrl: saleorApiUrl, + }, + }); + } + + setConfiguration( + saleorApiUrl: string, + configuration: { + appId: string; + indexNamePrefix?: string; + secretKey: string; + } + ) { + return this.prisma.algoliaConfiguration.upsert({ + where: { + saleorApiUrl: saleorApiUrl, + }, + create: { + saleorApiUrl, + appId: configuration.appId, + indexNamePrefix: configuration.indexNamePrefix, + secretKey: configuration.secretKey, + }, + update: { + saleorApiUrl, + appId: configuration.appId, + indexNamePrefix: configuration.indexNamePrefix, + secretKey: configuration.secretKey, + }, + }); + } +} + +export const algoliaConfigurationRepository = new AlgoliaConfigurationRepository(prisma); diff --git a/apps/search/src/instrumentation.ts b/apps/search/src/instrumentation.ts new file mode 100644 index 0000000..1625990 --- /dev/null +++ b/apps/search/src/instrumentation.ts @@ -0,0 +1,17 @@ +import { prisma } from "./db/prisma"; + +export const register = async () => { + if (process.env.RUN_WORKER_IN_NEXT_PROCESS === "true" && process.env.NEXT_RUNTIME === "nodejs") { + console.log("RUN_WORKER_IN_NEXT_PROCESS env is set, will inject worker to Next.js process"); + + await import("./worker/runner").catch(); + } + + if (process.env.NEXT_RUNTIME === "nodejs") { + prisma.$connect().catch((e: any) => { + console.error(e); + console.error("Cant connect to database, will exit"); + process.exit(1); + }); + } +}; diff --git a/apps/search/src/lib/algolia/getAlgoliaConfiguration.ts b/apps/search/src/lib/algolia/getAlgoliaConfiguration.ts index 7d8d1b6..9139438 100644 --- a/apps/search/src/lib/algolia/getAlgoliaConfiguration.ts +++ b/apps/search/src/lib/algolia/getAlgoliaConfiguration.ts @@ -1,64 +1,20 @@ import { AuthData } from "@saleor/app-sdk/APL"; -import { createDebug } from "../debug"; -import { createClient } from "../graphql"; -import { createSettingsManager } from "../metadata"; +import { algoliaConfigurationRepository } from "../../domain/algolia-configuration/AlgoliaConfigurationRepository"; interface GetAlgoliaConfigurationArgs { authData: AuthData; } -const debug = createDebug("getAlgoliaConfiguration"); - export const getAlgoliaConfiguration = async ({ authData }: GetAlgoliaConfigurationArgs) => { - const client = createClient(authData.saleorApiUrl, async () => - Promise.resolve({ token: authData.token }) + const configuration = await algoliaConfigurationRepository.getConfiguration( + authData.saleorApiUrl ); - const settings = createSettingsManager(client); - - try { - const secretKey = await settings.get("secretKey", authData.domain); - - if (!secretKey?.length) { - return { - errors: [ - { - message: - "Missing secret key to the Algolia API. Please, configure the application first.", - }, - ], + return configuration + ? { + settings: configuration, + } + : { + errors: [{ message: "Configuration doesnt exist" }], }; - } - - const appId = await settings.get("appId", authData.domain); - - if (!appId?.length) { - return { - errors: [ - { - message: "Missing App ID to the Algolia API. Please, configure the application first.", - }, - ], - }; - } - - const indexNamePrefix = (await settings.get("indexNamePrefix", authData.domain)) || ""; - - debug("Configuration fetched"); - return { - settings: { - appId, - secretKey, - indexNamePrefix, - }, - }; - } catch (error) { - debug("Unexpected error during fetching the configuration"); - if (error instanceof Error) { - debug(error.message); - } - return { - errors: [{ message: "Couldn't fetch the settings from the API" }], - }; - } }; diff --git a/apps/search/src/lib/metadata.ts b/apps/search/src/lib/metadata.ts deleted file mode 100644 index 3225e47..0000000 --- a/apps/search/src/lib/metadata.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { EncryptedMetadataManager, MetadataEntry, SettingsManager } from "@saleor/app-sdk/settings-manager"; - -import { FetchAppDetailsDocument, FetchAppDetailsQuery, UpdateAppMetadataDocument } from "../../generated/graphql"; -import { settingsManagerSecretKey } from "../../saleor-app"; -import { SimpleGraphqlClient } from "./graphql"; - - -/* - * Function is using urql graphql client to fetch all available metadata. - * Before returning query result, we are transforming response to list of objects with key and value fields - * which can be used by the manager. - * Result of this query is cached by the manager. - */ -export async function fetchAllMetadata(client: SimpleGraphqlClient): Promise { - const { error, data } = await client - .query(FetchAppDetailsDocument, {}) - .toPromise(); - - if (error) { - console.debug("Error during fetching the metadata: ", error); - return []; - } - - return data?.app?.privateMetadata.map((md) => ({ key: md.key, value: md.value })) || []; -} - -/* - * Mutate function takes urql client and metadata entries, and construct mutation to the API. - * Before data are send, additional query for required App ID is made. - * The manager will use updated entries returned by this mutation to update it's cache. - */ -export async function mutateMetadata(client: SimpleGraphqlClient, metadata: MetadataEntry[]) { - // to update the metadata, ID is required - const { error: idQueryError, data: idQueryData } = await client - .query(FetchAppDetailsDocument, {}) - .toPromise(); - - if (idQueryError) { - console.debug("Could not fetch the app id: ", idQueryError); - throw new Error( - "Could not fetch the app id. Please check if auth data for the client are valid." - ); - } - - const appId = idQueryData?.app?.id; - - if (!appId) { - console.debug("Missing app id"); - throw new Error("Could not fetch the app ID"); - } - - const { error: mutationError, data: mutationData } = await client - .mutation(UpdateAppMetadataDocument, { - id: appId, - input: metadata, - }) - .toPromise(); - - if (mutationError) { - console.debug("Mutation error: ", mutationError); - throw new Error(`Mutation error: ${mutationError.message}`); - } - - return ( - mutationData?.updatePrivateMetadata?.item?.privateMetadata.map((md) => ({ - key: md.key, - value: md.value, - })) || [] - ); -} - -export const createSettingsManager = (client: SimpleGraphqlClient): SettingsManager => { - /* - * EncryptedMetadataManager gives you interface to manipulate metadata and cache values in memory. - * We recommend it for production, because all values are encrypted. - * If your use case require plain text values, you can use MetadataManager. - */ - return new EncryptedMetadataManager({ - // Secret key should be randomly created for production and set as environment variable - encryptionKey: settingsManagerSecretKey, - fetchMetadata: () => fetchAllMetadata(client), - mutateMetadata: (metadata) => mutateMetadata(client, metadata), - }); -}; diff --git a/apps/search/src/pages/api/configuration.ts b/apps/search/src/pages/api/configuration.ts index 00dbcf3..971af83 100644 --- a/apps/search/src/pages/api/configuration.ts +++ b/apps/search/src/pages/api/configuration.ts @@ -1,45 +1,19 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import { SettingsManager } from "@saleor/app-sdk/settings-manager"; import { createClient } from "../../lib/graphql"; -import { createSettingsManager } from "../../lib/metadata"; import { saleorApp } from "../../../saleor-app"; import { createProtectedHandler, ProtectedHandlerContext } from "@saleor/app-sdk/handlers/next"; import { createLogger } from "../../lib/logger"; -import { AppConfigurationFields } from "../../domain/configuration"; +import { AppConfigurationFields, AppConfigurationSchema } from "../../domain/configuration"; import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider"; import { WebhookActivityTogglerService } from "../../domain/WebhookActivityToggler.service"; +import { algoliaConfigurationRepository } from "../../domain/algolia-configuration/AlgoliaConfigurationRepository"; const logger = createLogger({ handler: "api/configuration", }); -export interface SettingsApiResponse { - success: boolean; - data?: AppConfigurationFields; -} - -const sendResponse = async ( - res: NextApiResponse, - statusCode: number, - settings: SettingsManager, - domain: string -) => { - const data = { - secretKey: (await settings.get("secretKey", domain)) || "", - appId: (await settings.get("appId", domain)) || "", - indexNamePrefix: (await settings.get("indexNamePrefix", domain)) || "", - }; - - logger.debug(data, "Will return following settings"); - - res.status(statusCode).json({ - success: statusCode === 200, - data, - }); -}; - export const handler = async ( req: NextApiRequest, res: NextApiResponse, @@ -53,15 +27,20 @@ export const handler = async ( const client = createClient(saleorApiUrl, async () => Promise.resolve({ token: token })); - const settings = createSettingsManager(client); - - const domain = new URL(saleorApiUrl).host; - + // todo extract endpoints, add trpc if (req.method === "GET") { logger.debug("Returning configuration"); - await sendResponse(res, 200, settings, domain); - return; + const configuration = await algoliaConfigurationRepository.getConfiguration(saleorApiUrl); + + return configuration + ? res.status(200).send({ + success: true, + data: AppConfigurationSchema.parse(configuration), // todo probably remove Zod at this point + }) + : res.status(404).send({ + success: false, + }); } else if (req.method === "POST") { logger.debug("Updating the configuration"); @@ -79,11 +58,11 @@ export const handler = async ( logger.debug("Algolia connection is ok. Will save settings"); - await settings.set([ - { key: "secretKey", value: secretKey || "", domain }, - { key: "appId", value: appId || "", domain }, - { key: "indexNamePrefix", value: indexNamePrefix || "", domain }, - ]); + const configuration = await algoliaConfigurationRepository.setConfiguration(saleorApiUrl, { + appId, + secretKey, + indexNamePrefix, + }); logger.debug("Settings set"); @@ -92,13 +71,14 @@ export const handler = async ( await webhooksToggler.enableOwnWebhooks(); logger.debug("Webhooks enabled"); + + return res.status(200).send({ + success: true, + data: AppConfigurationSchema.parse(configuration), // todo probably remove Zod at this point + }); } catch (e) { return res.status(400).end(); } - - await sendResponse(res, 200, settings, domain); - - return; } logger.error("Method not supported"); diff --git a/apps/search/src/pages/api/index-products.ts b/apps/search/src/pages/api/index-products.ts new file mode 100644 index 0000000..c132d5d --- /dev/null +++ b/apps/search/src/pages/api/index-products.ts @@ -0,0 +1,18 @@ +import { createProtectedHandler } from "@saleor/app-sdk/handlers/next"; +import { saleorApp } from "../../../saleor-app"; +import { runIndexSaleorProducts } from "../../worker/index-saleor-products/index-saleor-products"; + +export default createProtectedHandler( + async (req, res, ctx) => { + const job = await runIndexSaleorProducts({ + saleorApiUrl: ctx.authData.saleorApiUrl, + }); //todo handle error + + console.log("Added job"); + console.log(job.id); + + return res.status(200).end(); + }, + saleorApp.apl, + ["MANAGE_APPS", "MANAGE_PRODUCTS"] +); diff --git a/apps/search/src/pages/api/jobs.ts b/apps/search/src/pages/api/jobs.ts new file mode 100644 index 0000000..7f05cab --- /dev/null +++ b/apps/search/src/pages/api/jobs.ts @@ -0,0 +1,10 @@ +import { createProtectedHandler } from "@saleor/app-sdk/handlers/next"; +import { saleorApp } from "../../../saleor-app"; + +export default createProtectedHandler( + async (req, res) => { + // todo https://github.com/graphile/worker/issues/330 + }, + saleorApp.apl, + ["MANAGE_APPS"] +); diff --git a/apps/search/src/pages/api/webhooks-status.ts b/apps/search/src/pages/api/webhooks-status.ts index 966d339..d33e59e 100644 --- a/apps/search/src/pages/api/webhooks-status.ts +++ b/apps/search/src/pages/api/webhooks-status.ts @@ -3,15 +3,16 @@ import { saleorApp } from "../../../saleor-app"; import { createClient, SimpleGraphqlClient } from "../../lib/graphql"; import { FetchOwnWebhooksDocument } from "../../../generated/graphql"; import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider"; -import { createSettingsManager } from "../../lib/metadata"; import { IWebhookActivityTogglerService, WebhookActivityTogglerService, } from "../../domain/WebhookActivityToggler.service"; import { createLogger } from "../../lib/logger"; -import { SettingsManager } from "@saleor/app-sdk/settings-manager"; -import { Client } from "urql"; import { SearchProvider } from "../../lib/searchProvider"; +import { + AlgoliaConfigurationRepository, + algoliaConfigurationRepository, +} from "../../domain/algolia-configuration/AlgoliaConfigurationRepository"; const logger = createLogger({ service: "webhooksStatusHandler", @@ -21,7 +22,7 @@ const logger = createLogger({ * Simple dependency injection - factory injects all services, in tests everything can be configured without mocks */ type FactoryProps = { - settingsManagerFactory: (client: SimpleGraphqlClient) => SettingsManager; + algoliaConfigurationRepository: Pick; webhookActivityTogglerFactory: ( appId: string, client: SimpleGraphqlClient @@ -32,7 +33,7 @@ type FactoryProps = { export const webhooksStatusHandlerFactory = ({ - settingsManagerFactory, + algoliaConfigurationRepository, webhookActivityTogglerFactory, algoliaSearchProviderFactory, graphqlClientFactory, @@ -43,25 +44,17 @@ export const webhooksStatusHandlerFactory = */ const client = graphqlClientFactory(authData.saleorApiUrl, authData.token); const webhooksToggler = webhookActivityTogglerFactory(authData.appId, client); - const settingsManager = settingsManagerFactory(client); - const domain = new URL(authData.saleorApiUrl).host; - - const [secretKey, appId] = await Promise.all([ - settingsManager.get("secretKey", domain), - settingsManager.get("appId", domain), - ]); - - const settings = { secretKey, appId }; - - logger.debug(settings, "fetched settings"); + const configuration = await algoliaConfigurationRepository.getConfiguration( + authData.saleorApiUrl + ); /** * If settings are incomplete, disable webhooks * * TODO Extract config operations to domain/ */ - if (!settings.appId || !settings.secretKey) { + if (!configuration) { logger.debug("Settings not set, will disable webhooks"); await webhooksToggler.disableOwnWebhooks(); @@ -69,7 +62,10 @@ export const webhooksStatusHandlerFactory = /** * Otherwise, if settings are set, check in Algolia if tokens are valid */ - const algoliaService = algoliaSearchProviderFactory(settings.appId, settings.secretKey); + const algoliaService = algoliaSearchProviderFactory( + configuration.appId, + configuration.secretKey + ); try { logger.debug("Settings set, will ping Algolia"); @@ -105,7 +101,7 @@ export const webhooksStatusHandlerFactory = export default createProtectedHandler( webhooksStatusHandlerFactory({ - settingsManagerFactory: createSettingsManager, + algoliaConfigurationRepository: algoliaConfigurationRepository, webhookActivityTogglerFactory: function (appId, client) { return new WebhookActivityTogglerService(appId, client); }, diff --git a/apps/search/src/pages/api/webhooks/saleor/product_created.ts b/apps/search/src/pages/api/webhooks/saleor/product_created.ts index 7a8b8a1..5d47ab5 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_created.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_created.ts @@ -41,15 +41,17 @@ export const handler: NextWebhookApiHandler = async (req, res, c logger.warn("Aborting due to lack of settings"); logger.debug(errors); + const error = (errors && errors[0] && errors[0].message) ?? "Unknown error"; + return res.status(400).json({ - message: errors[0].message, + message: error, }); } const searchProvider = new AlgoliaSearchProvider({ appId: settings.appId, apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, + indexNamePrefix: settings.indexNamePrefix ?? undefined, }); const { product } = context.payload; diff --git a/apps/search/src/pages/api/webhooks/saleor/product_deleted.ts b/apps/search/src/pages/api/webhooks/saleor/product_deleted.ts index a965c36..8981fb2 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_deleted.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_deleted.ts @@ -42,15 +42,17 @@ export const handler: NextWebhookApiHandler = async (req, res, c logger.warn("Aborting due to lack of settings"); logger.debug(errors); + const error = (errors && errors[0] && errors[0].message) ?? "Unknown error"; + return res.status(400).json({ - message: errors[0].message, + message: error, }); } const searchProvider = new AlgoliaSearchProvider({ appId: settings.appId, apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, + indexNamePrefix: settings.indexNamePrefix ?? undefined, }); const { product } = context.payload; diff --git a/apps/search/src/pages/api/webhooks/saleor/product_updated.ts b/apps/search/src/pages/api/webhooks/saleor/product_updated.ts index a2994a5..7d32c8b 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_updated.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_updated.ts @@ -41,15 +41,17 @@ export const handler: NextWebhookApiHandler = async (req, res, c logger.warn("Aborting due to lack of settings"); logger.debug(errors); + const error = (errors && errors[0] && errors[0].message) ?? "Unknown error"; + return res.status(400).json({ - message: errors[0].message, + message: error, }); } const searchProvider = new AlgoliaSearchProvider({ appId: settings.appId, apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, + indexNamePrefix: settings.indexNamePrefix ?? undefined, }); const { product } = context.payload; diff --git a/apps/search/src/pages/api/webhooks/saleor/product_variant_created.ts b/apps/search/src/pages/api/webhooks/saleor/product_variant_created.ts index a569974..427ed88 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_variant_created.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_variant_created.ts @@ -44,15 +44,17 @@ export const handler: NextWebhookApiHandler = async (req, if (errors?.length || !settings) { logger.warn("Aborting due to lack of settings"); logger.debug(errors); + const error = (errors && errors[0] && errors[0].message) ?? "Unknown error"; + return res.status(400).json({ - message: errors[0].message, + message: error, }); } const searchProvider = new AlgoliaSearchProvider({ appId: settings.appId, apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, + indexNamePrefix: settings.indexNamePrefix ?? undefined, }); const { productVariant } = context.payload; diff --git a/apps/search/src/pages/api/webhooks/saleor/product_variant_deleted.ts b/apps/search/src/pages/api/webhooks/saleor/product_variant_deleted.ts index 58fbed6..fc79874 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_variant_deleted.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_variant_deleted.ts @@ -45,15 +45,17 @@ export const handler: NextWebhookApiHandler = async (req, logger.warn("Aborting due to lack of settings"); logger.debug(errors); + const error = (errors && errors[0] && errors[0].message) ?? "Unknown error"; + return res.status(400).json({ - message: errors[0].message, + message: error, }); } const searchProvider = new AlgoliaSearchProvider({ appId: settings.appId, apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, + indexNamePrefix: settings.indexNamePrefix ?? undefined, }); const { productVariant } = context.payload; diff --git a/apps/search/src/pages/api/webhooks/saleor/product_variant_updated.ts b/apps/search/src/pages/api/webhooks/saleor/product_variant_updated.ts index 14414d2..7bb33cf 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_variant_updated.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_variant_updated.ts @@ -45,15 +45,17 @@ export const handler: NextWebhookApiHandler = async (req, logger.warn("Aborting due to lack of settings"); logger.debug(errors); + const error = (errors && errors[0] && errors[0].message) ?? "Unknown error"; + return res.status(400).json({ - message: errors[0].message, + message: error, }); } const searchProvider = new AlgoliaSearchProvider({ appId: settings.appId, apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, + indexNamePrefix: settings.indexNamePrefix ?? undefined, }); const { productVariant } = context.payload; diff --git a/apps/search/src/worker/index-saleor-products/get-products-and-send-to-algolia.ts b/apps/search/src/worker/index-saleor-products/get-products-and-send-to-algolia.ts new file mode 100644 index 0000000..8a23c1c --- /dev/null +++ b/apps/search/src/worker/index-saleor-products/get-products-and-send-to-algolia.ts @@ -0,0 +1,93 @@ +import { saleorApp } from "../../../saleor-app"; +import { createClient } from "../../lib/graphql"; +import { algoliaConfigurationRepository } from "../../domain/algolia-configuration/AlgoliaConfigurationRepository"; +import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider"; +import { + ChannelsDocument, + ProductsDataForImportDocument, + ProductsDataForImportQuery, +} from "../../../generated/graphql"; +import { Client } from "urql"; + +export type Products = NonNullable< + ProductsDataForImportQuery["products"] +>["edges"][number]["node"][]; + +const getChannels = (client: Client) => client.query(ChannelsDocument, {}).toPromise(); + +const PER_PAGE = 100; + +/** + * TODO - refactor and split into small tested chunks, not a scope of a POC + */ +export const getProductsAndSendToAlgolia = async (saleorApiUrl: string) => { + let products: Products = []; + + const authData = await saleorApp.apl.get(saleorApiUrl); + + if (!authData) { + throw new Error(); + } + + const client = createClient(authData.saleorApiUrl, async () => ({ + token: authData.token, + })); + + const getProducts = async (channelSlug: string, cursor: string): Promise => { + const response = await client + .query(ProductsDataForImportDocument, { + after: cursor, + first: PER_PAGE, + channel: channelSlug!, + }) + .toPromise(); + + const newProducts = response?.data?.products?.edges.map((e) => e.node) ?? []; + + if (newProducts.length > 0) { + products = [...products, ...newProducts]; + } + if ( + response?.data?.products?.pageInfo.hasNextPage && + response?.data?.products?.pageInfo.endCursor + ) { + // get next page of products + return getProducts(channelSlug, response.data.products?.pageInfo.endCursor); + } else { + // do nothing + return; + } + }; + + await (async () => { + const channels = await getChannels(client); + // get all products for each channel + + await channels.data?.channels?.reduce(async (acc, channel) => { + await acc; + await getProducts(channel.slug, ""); + }, Promise.resolve()); + })(); + + const configuration = await algoliaConfigurationRepository.getConfiguration( + authData.saleorApiUrl + ); // todo handle error + + const algolia = new AlgoliaSearchProvider({ + appId: configuration!.appId, + apiKey: configuration!.secretKey, + indexNamePrefix: configuration!.indexNamePrefix ?? undefined, + }); + + let currentProductIndex = 0; + + await (async () => { + const productsBatchStartIndex = currentProductIndex; + const productsBatchEndIndex = Math.min(currentProductIndex + PER_PAGE, products.length); + const productsBatch = products.slice(productsBatchStartIndex, productsBatchEndIndex); + + await algolia.updatedBatchProducts(productsBatch); + + currentProductIndex = productsBatchEndIndex; + })(); +}; diff --git a/apps/search/src/worker/index-saleor-products/index-saleor-products.ts b/apps/search/src/worker/index-saleor-products/index-saleor-products.ts new file mode 100644 index 0000000..0af6598 --- /dev/null +++ b/apps/search/src/worker/index-saleor-products/index-saleor-products.ts @@ -0,0 +1,42 @@ +import { Task } from "graphile-worker/dist/interfaces"; +import { z } from "zod"; +import { getProductsAndSendToAlgolia } from "./get-products-and-send-to-algolia"; +import { getWorkerUtils } from "../worker-utils"; + +const payloadSchema = z.object({ + saleorApiUrl: z.string().url(), +}); + +/** + * TODO Is it secure to pass only saleorApiUrl + * + * TODO Refactor to extract all product fetching etc + */ +export const IndexSaleorProducts: Task = async (payload, helpers) => { + /** + * Parse payload - in graphile its always unknown, so its a good place to ensure its correct + */ + const typedPayload = payloadSchema.parse(payload); + + /** + * Perform some business logic + */ + await getProductsAndSendToAlgolia(typedPayload.saleorApiUrl); +}; + +export const IndexSaleorProductsJobName = "IndexSaleorProducts"; + +/** + * Factory that pushed job to the worker + * + * https://github.com/graphile/worker#makeworkerutilsoptions-workerutilsoptions-promiseworkerutils + */ +export const runIndexSaleorProducts = async (payload: z.infer) => { + const utils = await getWorkerUtils(); + + await utils.migrate(); + + return utils.addJob(IndexSaleorProductsJobName, payload).finally(() => { + return utils.release(); + }); +}; diff --git a/apps/search/src/worker/runner.ts b/apps/search/src/worker/runner.ts new file mode 100644 index 0000000..352cbb1 --- /dev/null +++ b/apps/search/src/worker/runner.ts @@ -0,0 +1,30 @@ +import { IndexSaleorProducts } from "./index-saleor-products/index-saleor-products"; + +require("dotenv").config(); +const { run } = require("graphile-worker"); + +/** + * todo probably use another DB so Prisma will not destroy queue? + * + * how it will expose itself to kubernetes + */ +async function main() { + // Run a worker to execute jobs: + const runner = await run({ + connectionString: process.env.DATABASE_URL as string, + concurrency: 5, + // Install signal handlers for graceful shutdown on SIGINT, SIGTERM, etc + noHandleSignals: false, + pollInterval: 1000, + taskList: { + IndexSaleorProducts, + }, + }); + + await runner.promise; +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/apps/search/src/worker/worker-utils.ts b/apps/search/src/worker/worker-utils.ts new file mode 100644 index 0000000..2d1c92f --- /dev/null +++ b/apps/search/src/worker/worker-utils.ts @@ -0,0 +1,18 @@ +import { makeWorkerUtils, WorkerUtils } from "graphile-worker"; + +/** + * Ensure Singleton + */ +let _workerUtils: WorkerUtils | null = null; + +export const getWorkerUtils = async () => { + if (_workerUtils) { + return _workerUtils; + } + + _workerUtils = await makeWorkerUtils({ + connectionString: process.env.DATABASE_URL, + }); + + return _workerUtils; +}; diff --git a/apps/search/tsconfig.json b/apps/search/tsconfig.json index 6d58322..a2accf2 100644 --- a/apps/search/tsconfig.json +++ b/apps/search/tsconfig.json @@ -16,5 +16,5 @@ "incremental": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "src/worker/runner.ts", "src/worker/worker-utils.ts"] } diff --git a/apps/search/turbo.json b/apps/search/turbo.json index 1071973..1846a71 100644 --- a/apps/search/turbo.json +++ b/apps/search/turbo.json @@ -15,7 +15,10 @@ "SENTRY_PROJECT", "SENTRY_DSN", "SENTRY_ORG", - "NEXT_PUBLIC_VERCEL_ENV" + "NEXT_PUBLIC_VERCEL_ENV", + "DATABASE_URL", + "RUN_WORKER_IN_NEXT_PROCESS", + "NEXT_RUNTIME" ] } } diff --git a/docker-compose.db.yml b/docker-compose.db.yml new file mode 100644 index 0000000..99435b5 --- /dev/null +++ b/docker-compose.db.yml @@ -0,0 +1,23 @@ +# This compose contains only Database required for local development. +# App can be run locally without the container +# +# TODO Include app and worker services for local development with fullstack setup (and prod setup) + +version: '3' + +services: + postgres: + container_name: search_app_postgres + image: postgres # Todo maybe some alpine image? + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + PGDATA: /data/postgres + volumes: + - postgres:/data/postgres + ports: + - "5432:5432" + restart: unless-stopped + +volumes: + postgres: \ No newline at end of file diff --git a/docker-compose.search.prod.yml b/docker-compose.search.prod.yml new file mode 100644 index 0000000..4982455 --- /dev/null +++ b/docker-compose.search.prod.yml @@ -0,0 +1,57 @@ +version: '3' + +services: + postgres: + container_name: search_app_postgres + image: postgres # Todo maybe some alpine image? + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + PGDATA: /data/postgres + volumes: + - postgres:/data/postgres + ports: + - "5432:5432" + restart: unless-stopped + networks: + - saleor-app-search + saleor-app-search: + container_name: saleor-app-search + depends_on: + - postgres + build: + context: . + dockerfile: apps/search/app.prod.Dockerfile + args: + DATABASE_URL: "postgres://postgres:postgres@postgres/postgres" + environment: + APP_DEBUG: "info" + DATABASE_URL: "postgres://postgres:postgres@postgres/postgres" + restart: always +# command: TODO +# - npx prisma migrate deploy +# - node apps/search/server.js +# entrypoint: ["/bin/bash", "./apps/search/run-app.sh"] + ports: + - 3000:3000 + networks: + - saleor-app-search +# saleor-app-search-worker: +# container_name: saleor-app-search-worker +# depends_on: +# - postgres +# build: +# context: . # In examples its ./next-app, we can do that too todo +# dockerfile: worker.prod.Dockerfile +# environment: +# DATABASE_URL: "postgres://postgres:postgres@postgres/postgres" +# restart: always +# networks: +# - saleor-app-search +volumes: + postgres: + + +networks: + saleor-app-search: + driver: bridge \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04a6def..0319689 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1170,6 +1170,9 @@ importers: '@hookform/resolvers': specifier: ^3.1.0 version: 3.1.0(react-hook-form@7.43.9) + '@prisma/client': + specifier: ^4.15.0 + version: 4.15.0(prisma@4.15.0) '@saleor/app-sdk': specifier: 0.39.1 version: 0.39.1(next@13.3.0)(react-dom@18.2.0)(react@18.2.0) @@ -1203,6 +1206,12 @@ importers: debug: specifier: ^4.3.4 version: 4.3.4 + dotenv: + specifier: ^16.1.4 + version: 16.1.4 + graphile-worker: + specifier: ^0.13.0 + version: 0.13.0 graphql: specifier: 16.6.0 version: 16.6.0 @@ -1221,6 +1230,9 @@ importers: pino-pretty: specifier: ^10.0.0 version: 10.0.0 + prisma: + specifier: ^4.15.0 + version: 4.15.0 react: specifier: 18.2.0 version: 18.2.0 @@ -1279,6 +1291,9 @@ importers: '@vitejs/plugin-react': specifier: 4.0.0 version: 4.0.0(vite@4.3.9) + concurrently: + specifier: ^8.1.0 + version: 8.1.0 eslint: specifier: 8.42.0 version: 8.42.0 @@ -1288,6 +1303,12 @@ importers: node-mocks-http: specifier: ^1.12.2 version: 1.12.2 + tsup: + specifier: ^6.7.0 + version: 6.7.0(typescript@5.1.3) + tsx: + specifier: ^3.12.7 + version: 3.12.7 typescript: specifier: 5.1.3 version: 5.1.3 @@ -4921,6 +4942,27 @@ packages: resolution: {integrity: sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==} dev: false + /@esbuild-kit/cjs-loader@2.4.2: + resolution: {integrity: sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==} + dependencies: + '@esbuild-kit/core-utils': 3.1.0 + get-tsconfig: 4.4.0 + dev: true + + /@esbuild-kit/core-utils@3.1.0: + resolution: {integrity: sha512-Uuk8RpCg/7fdHSceR1M6XbSZFSuMrxcePFuGgyvsBn+u339dk5OeL4jv2EojwTN2st/unJGsVm4qHWjWNmJ/tw==} + dependencies: + esbuild: 0.17.17 + source-map-support: 0.5.21 + dev: true + + /@esbuild-kit/esm-loader@2.5.5: + resolution: {integrity: sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==} + dependencies: + '@esbuild-kit/core-utils': 3.1.0 + get-tsconfig: 4.4.0 + dev: true + /@esbuild/android-arm64@0.17.17: resolution: {integrity: sha512-jaJ5IlmaDLFPNttv0ofcwy/cfeY4bh/n705Tgh+eLObbGtQBK3EPAu+CzL95JVE4nFAliyrnEu0d32Q5foavqg==} engines: {node: '>=12'} @@ -5440,6 +5482,10 @@ packages: transitivePeerDependencies: - '@types/react' + /@graphile/logger@0.2.0: + resolution: {integrity: sha512-jjcWBokl9eb1gVJ85QmoaQ73CQ52xAaOCF29ukRbYNl6lY+ts0ErTaDYOBlejcbUs2OpaiqYLO5uDhyLFzWw4w==} + dev: false + /@graphql-codegen/cli@3.2.2(@babel/core@7.22.1)(@types/node@18.15.3)(graphql@16.6.0): resolution: {integrity: sha512-u+dm/SW1heLnUL4Tyf5Uv0AxOFhTCmUPHKwRLq2yE8MPhv7+Ti4vxxUP/XGoaMNRuHlN37wLI7tpFLV1Hhm22Q==} hasBin: true @@ -5930,7 +5976,7 @@ packages: '@types/jsonwebtoken': 9.0.1 chalk: 4.1.2 debug: 4.3.4 - dotenv: 16.0.3 + dotenv: 16.1.4 graphql: 16.6.0 graphql-request: 5.2.0(graphql@16.6.0) http-proxy-agent: 5.0.0 @@ -6768,6 +6814,29 @@ packages: resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} dev: false + /@prisma/client@4.15.0(prisma@4.15.0): + resolution: {integrity: sha512-xnROvyABcGiwqRNdrObHVZkD9EjkJYHOmVdlKy1yGgI+XOzvMzJ4tRg3dz1pUlsyhKxXGCnjIQjWW+2ur+YXuw==} + engines: {node: '>=14.17'} + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + '@prisma/engines-version': 4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944 + prisma: 4.15.0 + dev: false + + /@prisma/engines-version@4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944: + resolution: {integrity: sha512-sVOig4tjGxxlYaFcXgE71f/rtFhzyYrfyfNFUsxCIEJyVKU9rdOWIlIwQ2NQ7PntvGnn+x0XuFo4OC1jvPJKzg==} + dev: false + + /@prisma/engines@4.15.0: + resolution: {integrity: sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA==} + requiresBuild: true + dev: false + /@radix-ui/number@1.0.0: resolution: {integrity: sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==} dependencies: @@ -9464,7 +9533,7 @@ packages: engines: {node: '>=12'} dependencies: '@babel/code-frame': 7.21.4 - '@babel/runtime': 7.20.13 + '@babel/runtime': 7.22.3 '@types/aria-query': 5.0.1 aria-query: 5.1.3 chalk: 4.1.2 @@ -9980,6 +10049,14 @@ packages: /@types/parse-json@4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} + /@types/pg@8.10.1: + resolution: {integrity: sha512-AmEHA/XxMxemQom5iDwP62FYNkv+gDDnetRG7v2N2dPtju7UKI7FknUimcZo7SodKTHtckYPzaTqUEvUKbVJEA==} + dependencies: + '@types/node': 18.15.3 + pg-protocol: 1.6.0 + pg-types: 4.0.1 + dev: false + /@types/pikaday@1.7.4: resolution: {integrity: sha512-0KsHVyw5pTG829nqG4IRu7m+BFQlFEBdbE/1i3S5182HeKUKv1uEW0gyEmkJVp5i4IV+9pyh23O83+KpRkSQbw==} dependencies: @@ -10881,6 +10958,10 @@ packages: engines: {node: '>=12'} dev: true + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -11535,6 +11616,11 @@ packages: engines: {node: '>=0.10'} dev: false + /buffer-writer@2.0.0: + resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} + engines: {node: '>=4'} + dev: false + /buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -11553,6 +11639,16 @@ packages: engines: {node: '>=0.2.0'} dev: false + /bundle-require@4.0.1(esbuild@0.17.17): + resolution: {integrity: sha512-9NQkRHlNdNpDBGmLpngF3EFDcwodhMUuLz9PaWYciVcQF9SE4LFjM2DB/xV1Li5JiuDMv7ZUWuC3rGbqR0MAXQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.17' + dependencies: + esbuild: 0.17.17 + load-tsconfig: 0.2.5 + dev: true + /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -12030,7 +12126,6 @@ packages: /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - dev: false /commander@5.1.0: resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} @@ -12127,6 +12222,22 @@ packages: semver: 7.5.1 well-known-symbols: 2.0.0 + /concurrently@8.1.0: + resolution: {integrity: sha512-0AB6eOAtaW/r/kX2lCdolaWtT191ICeuJjEJvI9hT3zbPFuZ/iZaJwMRKwbuwADome7OKxk73L7od+fsveZ7tA==} + engines: {node: ^14.13.0 || >=16.0.0} + hasBin: true + dependencies: + chalk: 4.1.2 + date-fns: 2.29.3 + lodash: 4.17.21 + rxjs: 7.8.0 + shell-quote: 1.8.0 + spawn-command: 0.0.2-1 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + dev: true + /config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} dependencies: @@ -12409,7 +12520,6 @@ packages: /date-fns@2.29.3: resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==} engines: {node: '>=0.11'} - dev: false /date-time@3.1.0: resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} @@ -12764,6 +12874,10 @@ packages: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} + /dotenv@16.1.4: + resolution: {integrity: sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw==} + engines: {node: '>=12'} + /dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} @@ -14200,6 +14314,17 @@ packages: path-scurry: 1.9.2 dev: true + /glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + /glob@7.1.7: resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} dependencies: @@ -14308,6 +14433,24 @@ packages: /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + /graphile-worker@0.13.0: + resolution: {integrity: sha512-8Hl5XV6hkabZRhYzvbUfvjJfPFR5EPxYRVWlzQC2rqYHrjULTLBgBYZna5R9ukbnsbWSvn4vVrzOBIOgIC1jjw==} + engines: {node: '>=10.0.0'} + hasBin: true + dependencies: + '@graphile/logger': 0.2.0 + '@types/debug': 4.1.7 + '@types/pg': 8.10.1 + chokidar: 3.5.3 + cosmiconfig: 7.1.0 + json5: 2.2.3 + pg: 8.11.0 + tslib: 2.5.3 + yargs: 16.2.0 + transitivePeerDependencies: + - pg-native + dev: false + /graphql-config@4.5.0(@types/node@18.15.3)(graphql@16.6.0): resolution: {integrity: sha512-x6D0/cftpLUJ0Ch1e5sj1TZn6Wcxx4oMfmhaG9shM0DKajA9iR+j1z86GSTQ19fShbGvrSSvbIQsHku6aQ6BBw==} engines: {node: '>= 10.0.0'} @@ -15307,7 +15450,6 @@ packages: /joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - dev: false /js-beautify@1.14.7: resolution: {integrity: sha512-5SOX1KXPFKx+5f6ZrPsIPEY7NwKeQz47n3jm2i+XeHx9MoRsfQenlOP13FQhWvg8JRS0+XLO6XYUQ2GX+q+T9A==} @@ -15657,7 +15799,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: app-root-dir: 1.0.2 - dotenv: 16.0.3 + dotenv: 16.1.4 dotenv-expand: 10.0.0 dev: true @@ -15790,6 +15932,11 @@ packages: wrap-ansi: 7.0.0 dev: true + /load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /load-yaml-file@0.2.0: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} @@ -15891,6 +16038,10 @@ packages: /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: true + /lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -16940,6 +17091,14 @@ packages: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} dev: true + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + /nano-css@5.3.5(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-vSB9X12bbNu4ALBu7nigJgRViZ6ja3OU7CeuiV1zMIbXOdmkLahgtPmh3GBOlDxbKY0CitqlPdOReGlBLSp+yg==} peerDependencies: @@ -17381,6 +17540,10 @@ packages: resolution: {integrity: sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==} dev: false + /obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + dev: false + /on-exit-leak-free@2.1.0: resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==} dev: false @@ -17544,6 +17707,10 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + /packet-reader@1.0.0: + resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} + dev: false + /pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} @@ -17717,6 +17884,88 @@ packages: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} dev: false + /pg-cloudflare@1.1.0: + resolution: {integrity: sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA==} + requiresBuild: true + dev: false + optional: true + + /pg-connection-string@2.6.0: + resolution: {integrity: sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg==} + dev: false + + /pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + dev: false + + /pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + dev: false + + /pg-pool@3.6.0(pg@8.11.0): + resolution: {integrity: sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==} + peerDependencies: + pg: '>=8.0' + dependencies: + pg: 8.11.0 + dev: false + + /pg-protocol@1.6.0: + resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} + dev: false + + /pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + dev: false + + /pg-types@4.0.1: + resolution: {integrity: sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==} + engines: {node: '>=10'} + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.2 + postgres-bytea: 3.0.0 + postgres-date: 2.0.1 + postgres-interval: 3.0.0 + postgres-range: 1.1.3 + dev: false + + /pg@8.11.0: + resolution: {integrity: sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + dependencies: + buffer-writer: 2.0.0 + packet-reader: 1.0.0 + pg-connection-string: 2.6.0 + pg-pool: 3.6.0(pg@8.11.0) + pg-protocol: 1.6.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.0 + dev: false + + /pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + dependencies: + split2: 4.1.0 + dev: false + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -17878,6 +18127,54 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + dev: false + + /postgres-array@3.0.2: + resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} + engines: {node: '>=12'} + dev: false + + /postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + dependencies: + obuf: 1.1.2 + dev: false + + /postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-date@2.0.1: + resolution: {integrity: sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==} + engines: {node: '>=12'} + dev: false + + /postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 + dev: false + + /postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + dev: false + + /postgres-range@1.1.3: + resolution: {integrity: sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==} + dev: false + /preferred-pm@3.0.3: resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} engines: {node: '>=10'} @@ -17935,6 +18232,15 @@ packages: prettier: 2.8.8 dev: true + /prisma@4.15.0: + resolution: {integrity: sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA==} + engines: {node: '>=14.17'} + hasBin: true + requiresBuild: true + dependencies: + '@prisma/engines': 4.15.0 + dev: false + /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -19452,6 +19758,13 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + /source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 + dev: true + /sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead @@ -19465,6 +19778,10 @@ packages: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} dev: false + /spawn-command@0.0.2-1: + resolution: {integrity: sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==} + dev: true + /spawndamnit@2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} dependencies: @@ -19776,6 +20093,20 @@ packages: resolution: {integrity: sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==} dev: false + /sucrase@3.32.0: + resolution: {integrity: sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==} + engines: {node: '>=8'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.5 + ts-interface-checker: 0.1.13 + dev: true + /superagent@3.8.1: resolution: {integrity: sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==} engines: {node: '>= 4.0'} @@ -19982,6 +20313,19 @@ packages: /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + /thread-stream@2.3.0: resolution: {integrity: sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==} dependencies: @@ -20128,6 +20472,12 @@ packages: /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + /tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.3.0 + dev: true + /tr46@3.0.0: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} @@ -20146,6 +20496,11 @@ packages: resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} dev: false + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + /trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} dev: false @@ -20167,6 +20522,10 @@ packages: resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} dev: false + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + /ts-log@2.2.5: resolution: {integrity: sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==} dev: true @@ -20197,6 +20556,42 @@ packages: /tslib@2.5.3: resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} + /tsup@6.7.0(typescript@5.1.3): + resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==} + engines: {node: '>=14.18'} + hasBin: true + peerDependencies: + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.1.0' + peerDependenciesMeta: + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 4.0.1(esbuild@0.17.17) + cac: 6.7.14 + chokidar: 3.5.3 + debug: 4.3.4 + esbuild: 0.17.17 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 3.1.4(postcss@8.4.21) + resolve-from: 5.0.0 + rollup: 3.23.0 + source-map: 0.8.0-beta.0 + sucrase: 3.32.0 + tree-kill: 1.2.2 + typescript: 5.1.3 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + /tsutils@3.21.0(typescript@5.1.3): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -20207,6 +20602,17 @@ packages: typescript: 5.1.3 dev: true + /tsx@3.12.7: + resolution: {integrity: sha512-C2Ip+jPmqKd1GWVQDvz/Eyc6QJbGfE7NrR3fx5BpEHMZsEHoIxHL1j+lKdGobr8ovEyqeNkPLSKp6SCSOt7gmw==} + hasBin: true + dependencies: + '@esbuild-kit/cjs-loader': 2.4.2 + '@esbuild-kit/core-utils': 3.1.0 + '@esbuild-kit/esm-loader': 2.5.5 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /tty-table@4.1.6: resolution: {integrity: sha512-kRj5CBzOrakV4VRRY5kUWbNYvo/FpOsz65DzI5op9P+cHov3+IqPbo1JE1ZnQGkHdZgNFDsrEjrfqqy/Ply9fw==} engines: {node: '>=8.0.0'} @@ -20218,7 +20624,7 @@ packages: smartwrap: 2.0.2 strip-ansi: 6.0.1 wcwidth: 1.0.1 - yargs: 17.6.2 + yargs: 17.7.2 /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -21178,6 +21584,10 @@ packages: /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + /webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: true + /webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -21261,6 +21671,14 @@ packages: tr46: 0.0.3 webidl-conversions: 3.0.1 + /whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: true + /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: @@ -21447,7 +21865,6 @@ packages: /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - dev: true /y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} @@ -21532,6 +21949,19 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 /yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} diff --git a/turbo.json b/turbo.json index 2678d8c..837a86f 100644 --- a/turbo.json +++ b/turbo.json @@ -8,6 +8,11 @@ "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**"] }, + "build:app": { + "env": ["NEXT_PUBLIC_VERCEL_ENV"], + "dependsOn": ["^build"], + "outputs": ["dist/**", ".next/**"] + }, "lint": { "inputs": ["src"], "outputs": []