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": []