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
This commit is contained in:
Lukasz Ostrowski 2023-06-05 09:25:02 +02:00
parent a8834a11fe
commit 7240f6efa7
39 changed files with 1044 additions and 301 deletions

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
node_modules/
**/.env

View file

@ -0,0 +1,2 @@
node_modules/
**/.env

View file

@ -2,3 +2,9 @@
SECRET_KEY=
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

1
apps/search/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
worker-dist/

View file

@ -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

View file

@ -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 = {

View file

@ -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": {

View file

@ -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");

View file

@ -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"

View file

@ -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
}

View file

View file

@ -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,
});

View file

@ -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 | boolean>(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 | boolean>(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 (
<Box __cursor={started ? "wait" : "auto"}>
<Box>
{searchProvider && algoliaConfigured ? (
<Box>
<Text variant={"heading"} as={"p"} marginBottom={4}>
Importing products & variants
</Text>
<Text as={"p"}>
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
</Text>
<Text marginBottom={8} variant={"bodyStrong"}>
Do not close the app - its running client-side
</Text>
<Box display={"flex"} justifyContent={"flex-end"}>
<Button disabled={started || !searchProvider} onClick={importProducts}>
Start importing
<Box display={"flex"} justifyContent={"flex-end"} marginTop={13}>
<Button onClick={() => fetch("/api/index-products")}>Start importing</Button>
<Button
onClick={() =>
fetch("/api/jobs")
.then((r: any) => r.json())
.then((jobs: unknown) => {
console.log(jobs);
})
}
>
Check status
</Button>
</Box>
</Box>
@ -89,29 +70,6 @@ export const ImportProductsToAlgolia = () => {
<Text>Configure Algolia first</Text>
</Box>
)}
{started && (
<div
style={{
marginTop: "20px",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
{countVariants(products, currentProductIndex)} /{" "}
{countVariants(products, products.length)}
<progress
value={currentProductIndex}
max={products.length}
style={{
height: "30px",
width: "500px",
maxWidth: "100%",
}}
/>
</div>
)}
</Box>
);
};

View file

@ -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!;

View file

@ -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();
});

View file

@ -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);

View file

@ -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);
});
}
};

View file

@ -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,
}
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.",
},
],
: {
errors: [{ message: "Configuration doesnt exist" }],
};
}
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" }],
};
}
};

View file

@ -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<MetadataEntry[]> {
const { error, data } = await client
.query<FetchAppDetailsQuery>(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),
});
};

View file

@ -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<SettingsApiResponse>,
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");

View file

@ -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"]
);

View file

@ -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"]
);

View file

@ -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<AlgoliaConfigurationRepository, "getConfiguration">;
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);
},

View file

@ -41,15 +41,17 @@ export const handler: NextWebhookApiHandler<ProductCreated> = 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;

View file

@ -42,15 +42,17 @@ export const handler: NextWebhookApiHandler<ProductDeleted> = 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;

View file

@ -41,15 +41,17 @@ export const handler: NextWebhookApiHandler<ProductUpdated> = 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;

View file

@ -44,15 +44,17 @@ export const handler: NextWebhookApiHandler<ProductVariantCreated> = 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;

View file

@ -45,15 +45,17 @@ export const handler: NextWebhookApiHandler<ProductVariantDeleted> = 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;

View file

@ -45,15 +45,17 @@ export const handler: NextWebhookApiHandler<ProductVariantUpdated> = 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;

View file

@ -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<void> => {
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;
})();
};

View file

@ -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<typeof payloadSchema>) => {
const utils = await getWorkerUtils();
await utils.migrate();
return utils.addJob(IndexSaleorProductsJobName, payload).finally(() => {
return utils.release();
});
};

View file

@ -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);
});

View file

@ -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;
};

View file

@ -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"]
}

View file

@ -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"
]
}
}

23
docker-compose.db.yml Normal file
View file

@ -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:

View file

@ -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

View file

@ -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==}

View file

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