Add tRPC to search app (#940)
* Add tRPC to search app * Implemented configuration endpoint in trpc * replace settings manager to use shared factory * replace configuration calls to trpc
This commit is contained in:
parent
2a1385bab1
commit
0f84985c98
24 changed files with 424 additions and 302 deletions
7
.changeset/sixty-ligers-fold.md
Normal file
7
.changeset/sixty-ligers-fold.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
"saleor-app-search": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Refactor: Introduced tRPC and re-implemented /api/configuration endpoint to the tRPC controller.
|
||||||
|
|
||||||
|
_This does not affect the end-user functionality_
|
|
@ -20,6 +20,10 @@
|
||||||
"@saleor/react-hook-form-macaw": "workspace:*",
|
"@saleor/react-hook-form-macaw": "workspace:*",
|
||||||
"@sentry/nextjs": "7.55.2",
|
"@sentry/nextjs": "7.55.2",
|
||||||
"@tanstack/react-query": "4.29.19",
|
"@tanstack/react-query": "4.29.19",
|
||||||
|
"@trpc/client": "10.34.0",
|
||||||
|
"@trpc/next": "10.34.0",
|
||||||
|
"@trpc/react-query": "10.34.0",
|
||||||
|
"@trpc/server": "10.34.0",
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
"@urql/exchange-auth": "^2.1.4",
|
"@urql/exchange-auth": "^2.1.4",
|
||||||
"algoliasearch": "4.19.1",
|
"algoliasearch": "4.19.1",
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { useAuthenticatedFetch } from "@saleor/app-sdk/app-bridge";
|
import { useAuthenticatedFetch } from "@saleor/app-sdk/app-bridge";
|
||||||
|
|
||||||
import { useForm } from "react-hook-form";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
import { fetchConfiguration } from "../lib/configuration";
|
|
||||||
import { Box, Button, Divider, Text } from "@saleor/macaw-ui/next";
|
import { Box, Button, Divider, Text } from "@saleor/macaw-ui/next";
|
||||||
import { Input } from "@saleor/react-hook-form-macaw";
|
import { Input } from "@saleor/react-hook-form-macaw";
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { AlgoliaSearchProvider } from "../lib/algolia/algoliaSearchProvider";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { AppConfigurationFields, AppConfigurationSchema } from "../domain/configuration";
|
import { AppConfigurationFields, AppConfigurationSchema } from "../domain/configuration";
|
||||||
|
import { AlgoliaSearchProvider } from "../lib/algolia/algoliaSearchProvider";
|
||||||
|
import { trpcClient } from "../modules/trpc/trpc-client";
|
||||||
|
|
||||||
export const AlgoliaConfigurationForm = () => {
|
export const AlgoliaConfigurationForm = () => {
|
||||||
const { notifyError, notifySuccess } = useDashboardNotification();
|
const { notifyError, notifySuccess } = useDashboardNotification();
|
||||||
|
@ -24,50 +24,32 @@ export const AlgoliaConfigurationForm = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const reactQueryClient = useQueryClient();
|
const reactQueryClient = useQueryClient();
|
||||||
/**
|
|
||||||
* TODO Extract to hook
|
const { isLoading: isQueryLoading, refetch: refetchConfig } =
|
||||||
*/
|
trpcClient.configuration.getConfig.useQuery(undefined, {
|
||||||
const { isLoading: isQueryLoading } = useQuery({
|
|
||||||
queryKey: ["configuration"],
|
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
setValue("secretKey", data?.secretKey || "");
|
setValue("secretKey", data?.secretKey || "");
|
||||||
setValue("appId", data?.appId || "");
|
setValue("appId", data?.appId || "");
|
||||||
setValue("indexNamePrefix", data?.indexNamePrefix || "");
|
setValue("indexNamePrefix", data?.indexNamePrefix || "");
|
||||||
},
|
},
|
||||||
queryFn: async () => fetchConfiguration(fetch),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate, isLoading: isMutationLoading } = useMutation(
|
const { mutate: setConfig, isLoading: isMutationLoading } =
|
||||||
async (conf: AppConfigurationFields) => {
|
trpcClient.configuration.setConfig.useMutation({
|
||||||
const resp = await fetch("/api/configuration", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(conf),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resp.status >= 200 && resp.status < 300) {
|
|
||||||
const data = (await resp.json()) as { data?: AppConfigurationFields };
|
|
||||||
|
|
||||||
return data.data;
|
|
||||||
}
|
|
||||||
throw new Error(`Server responded with status code ${resp.status}`);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
reactQueryClient.refetchQueries({
|
refetchConfig(),
|
||||||
queryKey: ["configuration"],
|
// todo migrate to trpc
|
||||||
}),
|
|
||||||
reactQueryClient.refetchQueries({
|
reactQueryClient.refetchQueries({
|
||||||
queryKey: ["webhooks-status"],
|
queryKey: ["webhooks-status"],
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
notifySuccess("Configuration saved!");
|
notifySuccess("Configuration saved!");
|
||||||
},
|
},
|
||||||
onError: async (data: Error) => {
|
onError: async (error) => {
|
||||||
notifyError("Could not save the configuration", data.message);
|
notifyError("Could not save the configuration", error.message);
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const onFormSubmit = handleSubmit(async (conf) => {
|
const onFormSubmit = handleSubmit(async (conf) => {
|
||||||
const client = new AlgoliaSearchProvider({
|
const client = new AlgoliaSearchProvider({
|
||||||
|
@ -80,7 +62,7 @@ export const AlgoliaConfigurationForm = () => {
|
||||||
await client.ping();
|
await client.ping();
|
||||||
setCredentialsValidationError(false);
|
setCredentialsValidationError(false);
|
||||||
|
|
||||||
mutate(conf);
|
setConfig(conf);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
trigger();
|
trigger();
|
||||||
setCredentialsValidationError(true);
|
setCredentialsValidationError(true);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { AlgoliaSearchProvider } from "../lib/algolia/algoliaSearchProvider";
|
import { AlgoliaSearchProvider } from "../lib/algolia/algoliaSearchProvider";
|
||||||
import { useConfiguration } from "../lib/configuration";
|
|
||||||
import { Products, useQueryAllProducts } from "./useQueryAllProducts";
|
import { Products, useQueryAllProducts } from "./useQueryAllProducts";
|
||||||
|
import { trpcClient } from "../modules/trpc/trpc-client";
|
||||||
|
|
||||||
const BATCH_SIZE = 100;
|
const BATCH_SIZE = 100;
|
||||||
|
|
||||||
|
@ -14,21 +14,21 @@ export const ImportProductsToAlgolia = () => {
|
||||||
|
|
||||||
const products = useQueryAllProducts(!started);
|
const products = useQueryAllProducts(!started);
|
||||||
|
|
||||||
const algoliaConfiguration = useConfiguration();
|
const { data: algoliaConfiguration } = trpcClient.configuration.getConfig.useQuery();
|
||||||
|
|
||||||
const searchProvider = useMemo(() => {
|
const searchProvider = useMemo(() => {
|
||||||
if (!algoliaConfiguration.data?.appId || !algoliaConfiguration.data.secretKey) {
|
if (!algoliaConfiguration?.appId || !algoliaConfiguration.secretKey) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new AlgoliaSearchProvider({
|
return new AlgoliaSearchProvider({
|
||||||
appId: algoliaConfiguration.data.appId,
|
appId: algoliaConfiguration.appId,
|
||||||
apiKey: algoliaConfiguration.data.secretKey,
|
apiKey: algoliaConfiguration.secretKey,
|
||||||
indexNamePrefix: algoliaConfiguration.data.indexNamePrefix,
|
indexNamePrefix: algoliaConfiguration.indexNamePrefix,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
algoliaConfiguration?.data?.appId,
|
algoliaConfiguration?.appId,
|
||||||
algoliaConfiguration?.data?.indexNamePrefix,
|
algoliaConfiguration?.indexNamePrefix,
|
||||||
algoliaConfiguration?.data?.secretKey,
|
algoliaConfiguration?.secretKey,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const importProducts = useCallback(() => {
|
const importProducts = useCallback(() => {
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useConfiguration } from "../lib/configuration";
|
|
||||||
import { TextLink } from "@saleor/apps-ui";
|
import { TextLink } from "@saleor/apps-ui";
|
||||||
import { useIndicesSetupMutation } from "../lib/useIndicesSetup";
|
import { useIndicesSetupMutation } from "../lib/useIndicesSetup";
|
||||||
|
import { trpcClient } from "../modules/trpc/trpc-client";
|
||||||
|
|
||||||
export const IndicesSettings = () => {
|
export const IndicesSettings = () => {
|
||||||
const algoliaConfiguration = useConfiguration();
|
const { data: algoliaConfiguration } = trpcClient.configuration.getConfig.useQuery();
|
||||||
const updateWebhooksMutation = useIndicesSetupMutation();
|
const updateWebhooksMutation = useIndicesSetupMutation();
|
||||||
|
|
||||||
const isConfigured = algoliaConfiguration.data?.appId && algoliaConfiguration.data?.secretKey;
|
const isConfigured = algoliaConfiguration?.appId && algoliaConfiguration?.secretKey;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
|
|
@ -15,7 +15,7 @@ export const getAlgoliaConfiguration = async ({ authData }: GetAlgoliaConfigurat
|
||||||
token: authData.token,
|
token: authData.token,
|
||||||
});
|
});
|
||||||
|
|
||||||
const settings = createSettingsManager(client);
|
const settings = createSettingsManager(client, authData.appId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const secretKey = await settings.get("secretKey", authData.domain);
|
const secretKey = await settings.get("secretKey", authData.domain);
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useAuthenticatedFetch } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { AppConfigurationFields } from "../domain/configuration";
|
|
||||||
|
|
||||||
export const fetchConfiguration = async (fetch: typeof window.fetch) => {
|
|
||||||
try {
|
|
||||||
const res = await fetch("/api/configuration");
|
|
||||||
const data = (await res.json()) as { data?: AppConfigurationFields };
|
|
||||||
|
|
||||||
return data.data;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useConfiguration = () => {
|
|
||||||
const fetch = useAuthenticatedFetch();
|
|
||||||
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["configuration"],
|
|
||||||
queryFn: () => fetchConfiguration(fetch),
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,91 +1,12 @@
|
||||||
import {
|
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||||
EncryptedMetadataManager,
|
import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared";
|
||||||
MetadataEntry,
|
|
||||||
SettingsManager,
|
|
||||||
} from "@saleor/app-sdk/settings-manager";
|
|
||||||
|
|
||||||
import {
|
|
||||||
FetchAppDetailsDocument,
|
|
||||||
FetchAppDetailsQuery,
|
|
||||||
UpdateAppMetadataDocument,
|
|
||||||
} from "../../generated/graphql";
|
|
||||||
import { settingsManagerSecretKey } from "../../saleor-app";
|
|
||||||
import { Client } from "urql";
|
import { Client } from "urql";
|
||||||
|
|
||||||
/*
|
export const createSettingsManager = (
|
||||||
* Function is using urql graphql client to fetch all available metadata.
|
client: Pick<Client, "query" | "mutation">,
|
||||||
* Before returning query result, we are transforming response to list of objects with key and value fields
|
appId: string,
|
||||||
* which can be used by the manager.
|
): SettingsManager => {
|
||||||
* Result of this query is cached by the manager.
|
const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!);
|
||||||
*/
|
|
||||||
export async function fetchAllMetadata(client: Client): Promise<MetadataEntry[]> {
|
|
||||||
const { error, data } = await client
|
|
||||||
.query<FetchAppDetailsQuery>(FetchAppDetailsDocument, {})
|
|
||||||
.toPromise();
|
|
||||||
|
|
||||||
if (error) {
|
return metadataManagerFactory.create(client, appId);
|
||||||
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: Client, 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: Client): 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),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { createLogger } from "@saleor/apps-shared";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { ChannelsDocument } from "../../../generated/graphql";
|
||||||
|
import { WebhookActivityTogglerService } from "../../domain/WebhookActivityToggler.service";
|
||||||
|
import { AppConfigurationFields, AppConfigurationSchema } from "../../domain/configuration";
|
||||||
|
import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider";
|
||||||
|
import { createSettingsManager } from "../../lib/metadata";
|
||||||
|
import { protectedClientProcedure } from "../trpc/protected-client-procedure";
|
||||||
|
import { router } from "../trpc/trpc-server";
|
||||||
|
|
||||||
|
const logger = createLogger({ name: "configuration.router" });
|
||||||
|
|
||||||
|
export const configurationRouter = router({
|
||||||
|
getConfig: protectedClientProcedure.query(async ({ ctx }) => {
|
||||||
|
const settingsManager = createSettingsManager(ctx.apiClient, ctx.appId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backwards compatbitility
|
||||||
|
*/
|
||||||
|
const domain = new URL(ctx.saleorApiUrl).host;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO - refactor to single config in one key
|
||||||
|
*/
|
||||||
|
const data: AppConfigurationFields = {
|
||||||
|
secretKey: (await settingsManager.get("secretKey", domain)) || "",
|
||||||
|
appId: (await settingsManager.get("appId", domain)) || "",
|
||||||
|
indexNamePrefix: (await settingsManager.get("indexNamePrefix", domain)) || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.debug("Will return config");
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
setConfig: protectedClientProcedure
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(AppConfigurationSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const { data: channelsData } = await ctx.apiClient.query(ChannelsDocument, {}).toPromise();
|
||||||
|
const channels = channelsData?.channels || [];
|
||||||
|
|
||||||
|
const algoliaClient = new AlgoliaSearchProvider({
|
||||||
|
appId: ctx.appId,
|
||||||
|
apiKey: input.secretKey,
|
||||||
|
indexNamePrefix: input.indexNamePrefix,
|
||||||
|
channels,
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsManager = createSettingsManager(ctx.apiClient, ctx.appId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backwards compatbitility
|
||||||
|
*/
|
||||||
|
const domain = new URL(ctx.saleorApiUrl).host;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug("Will ping Algolia");
|
||||||
|
await algoliaClient.ping();
|
||||||
|
|
||||||
|
logger.debug("Algolia connection is ok. Will save settings");
|
||||||
|
|
||||||
|
await settingsManager.set([
|
||||||
|
{ key: "secretKey", value: input.secretKey || "", domain },
|
||||||
|
{ key: "appId", value: input.appId || "", domain },
|
||||||
|
{ key: "indexNamePrefix", value: input.indexNamePrefix || "", domain },
|
||||||
|
]);
|
||||||
|
|
||||||
|
logger.debug("Settings set successfully");
|
||||||
|
|
||||||
|
const webhooksToggler = new WebhookActivityTogglerService(ctx.appId, ctx.apiClient);
|
||||||
|
|
||||||
|
await webhooksToggler.enableOwnWebhooks();
|
||||||
|
|
||||||
|
logger.debug("Webhooks enabled");
|
||||||
|
} catch (e) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
});
|
117
apps/search/src/modules/trpc/protected-client-procedure.ts
Normal file
117
apps/search/src/modules/trpc/protected-client-procedure.ts
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import { verifyJWT } from "@saleor/app-sdk/verify-jwt";
|
||||||
|
import { middleware, procedure } from "./trpc-server";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { ProtectedHandlerError } from "@saleor/app-sdk/handlers/next";
|
||||||
|
import { createGraphQLClient, logger } from "@saleor/apps-shared";
|
||||||
|
import { saleorApp } from "../../../saleor-app";
|
||||||
|
|
||||||
|
const attachAppToken = middleware(async ({ ctx, next }) => {
|
||||||
|
logger.debug("attachAppToken middleware");
|
||||||
|
|
||||||
|
if (!ctx.saleorApiUrl) {
|
||||||
|
logger.debug("ctx.saleorApiUrl not found, throwing");
|
||||||
|
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Missing saleorApiUrl in request",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const authData = await saleorApp.apl.get(ctx.saleorApiUrl);
|
||||||
|
|
||||||
|
if (!authData) {
|
||||||
|
logger.debug("authData not found, throwing 401");
|
||||||
|
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "Missing auth data",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
appToken: authData.token,
|
||||||
|
saleorApiUrl: authData.saleorApiUrl,
|
||||||
|
appId: authData.appId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateClientToken = middleware(async ({ ctx, next, meta }) => {
|
||||||
|
logger.debug(
|
||||||
|
{
|
||||||
|
permissions: meta?.requiredClientPermissions,
|
||||||
|
},
|
||||||
|
"Calling validateClientToken middleware with permissions required",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ctx.token) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Missing token in request. This middleware can be used only in frontend",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.appId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Missing appId in request. This middleware can be used after auth is attached",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.saleorApiUrl) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message:
|
||||||
|
"Missing saleorApiUrl in request. This middleware can be used after auth is attached",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.ssr) {
|
||||||
|
try {
|
||||||
|
logger.debug("trying to verify JWT token from frontend");
|
||||||
|
logger.debug({ token: ctx.token ? `${ctx.token[0]}...` : undefined });
|
||||||
|
|
||||||
|
await verifyJWT({
|
||||||
|
appId: ctx.appId,
|
||||||
|
token: ctx.token,
|
||||||
|
saleorApiUrl: ctx.saleorApiUrl,
|
||||||
|
requiredPermissions: meta?.requiredClientPermissions ?? [],
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug("JWT verification failed, throwing");
|
||||||
|
throw new ProtectedHandlerError("JWT verification failed: ", "JWT_VERIFICATION_FAILED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
...ctx,
|
||||||
|
saleorApiUrl: ctx.saleorApiUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct common graphQL client and attach it to the context
|
||||||
|
*
|
||||||
|
* Can be used only if called from the frontend (react-query),
|
||||||
|
* otherwise jwks validation will fail (if createCaller used)
|
||||||
|
*
|
||||||
|
* TODO Rethink middleware composition to enable safe server-side router calls
|
||||||
|
*/
|
||||||
|
export const protectedClientProcedure = procedure
|
||||||
|
.use(attachAppToken)
|
||||||
|
.use(validateClientToken)
|
||||||
|
.use(async ({ ctx, next }) => {
|
||||||
|
const client = createGraphQLClient({ saleorApiUrl: ctx.saleorApiUrl, token: ctx.appToken });
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
apiClient: client,
|
||||||
|
appToken: ctx.appToken,
|
||||||
|
saleorApiUrl: ctx.saleorApiUrl,
|
||||||
|
appId: ctx.appId!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
8
apps/search/src/modules/trpc/trpc-app-router.ts
Normal file
8
apps/search/src/modules/trpc/trpc-app-router.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { configurationRouter } from "../configuration/configuration.router";
|
||||||
|
import { router } from "./trpc-server";
|
||||||
|
|
||||||
|
export const appRouter = router({
|
||||||
|
configuration: configurationRouter,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AppRouter = typeof appRouter;
|
45
apps/search/src/modules/trpc/trpc-client.ts
Normal file
45
apps/search/src/modules/trpc/trpc-client.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { httpBatchLink } from "@trpc/client";
|
||||||
|
import { createTRPCNext } from "@trpc/next";
|
||||||
|
|
||||||
|
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
|
||||||
|
|
||||||
|
import { AppRouter } from "./trpc-app-router";
|
||||||
|
import { appBridgeInstance } from "../../pages/_app";
|
||||||
|
|
||||||
|
function getBaseUrl() {
|
||||||
|
if (typeof window !== "undefined") return "";
|
||||||
|
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
||||||
|
|
||||||
|
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const trpcClient = createTRPCNext<AppRouter>({
|
||||||
|
config({ ctx }) {
|
||||||
|
return {
|
||||||
|
links: [
|
||||||
|
httpBatchLink({
|
||||||
|
url: `${getBaseUrl()}/api/trpc`,
|
||||||
|
headers() {
|
||||||
|
const { token, saleorApiUrl } = appBridgeInstance?.getState() || {};
|
||||||
|
|
||||||
|
if (!token || !saleorApiUrl) {
|
||||||
|
console.error(
|
||||||
|
"Can't initialize tRPC client before establishing the App Bridge connection",
|
||||||
|
);
|
||||||
|
throw new Error("Token and Saleor API URL unknown");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Attach headers from app to client requests, so tRPC can add them to context
|
||||||
|
*/
|
||||||
|
[SALEOR_AUTHORIZATION_BEARER_HEADER]: appBridgeInstance?.getState().token,
|
||||||
|
[SALEOR_API_URL_HEADER]: appBridgeInstance?.getState().saleorApiUrl,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
queryClientConfig: { defaultOptions: { queries: { refetchOnWindowFocus: false } } },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
ssr: false,
|
||||||
|
});
|
30
apps/search/src/modules/trpc/trpc-context.ts
Normal file
30
apps/search/src/modules/trpc/trpc-context.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import * as trpcNext from "@trpc/server/adapters/next";
|
||||||
|
import { SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
|
||||||
|
import { inferAsyncReturnType } from "@trpc/server";
|
||||||
|
|
||||||
|
const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => {
|
||||||
|
const { host, "x-forwarded-proto": xForwardedProto = "http" } = headers;
|
||||||
|
|
||||||
|
const xForwardedProtos = Array.isArray(xForwardedProto)
|
||||||
|
? xForwardedProto.join(",")
|
||||||
|
: xForwardedProto;
|
||||||
|
const protocols = xForwardedProtos.split(",");
|
||||||
|
// prefer https over other protocols
|
||||||
|
const protocol = protocols.find((el) => el === "https") || protocols[0];
|
||||||
|
|
||||||
|
return `${protocol}://${host}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTrpcContext = async ({ res, req }: trpcNext.CreateNextContextOptions) => {
|
||||||
|
const baseUrl = getBaseUrl(req.headers);
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: req.headers[SALEOR_AUTHORIZATION_BEARER_HEADER] as string | undefined,
|
||||||
|
saleorApiUrl: req.headers[SALEOR_API_URL_HEADER] as string | undefined,
|
||||||
|
appId: undefined as undefined | string,
|
||||||
|
ssr: undefined as undefined | boolean,
|
||||||
|
baseUrl,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TrpcContext = inferAsyncReturnType<typeof createTrpcContext>;
|
31
apps/search/src/modules/trpc/trpc-server.ts
Normal file
31
apps/search/src/modules/trpc/trpc-server.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { initTRPC } from "@trpc/server";
|
||||||
|
import { TrpcContext } from "./trpc-context";
|
||||||
|
import { Permission } from "@saleor/app-sdk/types";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
interface Meta {
|
||||||
|
requiredClientPermissions?: Permission[];
|
||||||
|
updateWebhooks?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = initTRPC
|
||||||
|
.context<TrpcContext>()
|
||||||
|
.meta<Meta>()
|
||||||
|
.create({
|
||||||
|
errorFormatter({ shape, error }) {
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
data: {
|
||||||
|
...shape.data,
|
||||||
|
zodError:
|
||||||
|
error.code === "BAD_REQUEST" && error.cause instanceof ZodError
|
||||||
|
? error.cause.flatten()
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const router = t.router;
|
||||||
|
export const procedure = t.procedure;
|
||||||
|
export const middleware = t.middleware;
|
|
@ -9,12 +9,13 @@ import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
|
||||||
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
|
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
|
||||||
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
|
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
|
||||||
import { NoSSRWrapper } from "@saleor/apps-shared";
|
import { NoSSRWrapper } from "@saleor/apps-shared";
|
||||||
|
import { trpcClient } from "../modules/trpc/trpc-client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure instance is a singleton.
|
* Ensure instance is a singleton.
|
||||||
* TODO: This is React 18 issue, consider hiding this workaround inside app-sdk
|
* TODO: This is React 18 issue, consider hiding this workaround inside app-sdk
|
||||||
*/
|
*/
|
||||||
const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
|
export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
|
@ -44,4 +45,4 @@ function NextApp({ Component, pageProps }: AppProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NextApp;
|
export default trpcClient.withTRPC(NextApp);
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
|
||||||
|
|
||||||
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 { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider";
|
|
||||||
import { WebhookActivityTogglerService } from "../../domain/WebhookActivityToggler.service";
|
|
||||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
|
||||||
import { ChannelsDocument } from "../../../generated/graphql";
|
|
||||||
|
|
||||||
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,
|
|
||||||
ctx: ProtectedHandlerContext,
|
|
||||||
) => {
|
|
||||||
const {
|
|
||||||
authData: { token, saleorApiUrl },
|
|
||||||
} = ctx;
|
|
||||||
|
|
||||||
logger.debug({ saleorApiUrl }, "handler called");
|
|
||||||
|
|
||||||
const client = createGraphQLClient({
|
|
||||||
saleorApiUrl,
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
|
|
||||||
const settings = createSettingsManager(client);
|
|
||||||
|
|
||||||
const domain = new URL(saleorApiUrl).host;
|
|
||||||
|
|
||||||
if (req.method === "GET") {
|
|
||||||
logger.debug("Returning configuration");
|
|
||||||
|
|
||||||
await sendResponse(res, 200, settings, domain);
|
|
||||||
return;
|
|
||||||
} else if (req.method === "POST") {
|
|
||||||
logger.debug("Updating the configuration");
|
|
||||||
|
|
||||||
const { appId, secretKey, indexNamePrefix } = JSON.parse(req.body) as AppConfigurationFields;
|
|
||||||
|
|
||||||
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
|
|
||||||
const channels = channelsData?.channels || [];
|
|
||||||
|
|
||||||
const algoliaClient = new AlgoliaSearchProvider({
|
|
||||||
appId,
|
|
||||||
apiKey: secretKey,
|
|
||||||
indexNamePrefix: indexNamePrefix,
|
|
||||||
channels,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.debug("Will ping Algolia");
|
|
||||||
await algoliaClient.ping();
|
|
||||||
|
|
||||||
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 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
logger.debug("Settings set");
|
|
||||||
|
|
||||||
const webhooksToggler = new WebhookActivityTogglerService(ctx.authData.appId, client);
|
|
||||||
|
|
||||||
await webhooksToggler.enableOwnWebhooks();
|
|
||||||
|
|
||||||
logger.debug("Webhooks enabled");
|
|
||||||
} catch (e) {
|
|
||||||
return res.status(400).end();
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendResponse(res, 200, settings, domain);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.error("Method not supported");
|
|
||||||
|
|
||||||
res.status(405).end();
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createProtectedHandler(handler, saleorApp.apl, ["MANAGE_APPS", "MANAGE_PRODUCTS"]);
|
|
|
@ -20,7 +20,7 @@ const logger = createLogger({
|
||||||
* Simple dependency injection - factory injects all services, in tests everything can be configured without mocks
|
* Simple dependency injection - factory injects all services, in tests everything can be configured without mocks
|
||||||
*/
|
*/
|
||||||
type FactoryProps = {
|
type FactoryProps = {
|
||||||
settingsManagerFactory: (client: Client) => SettingsManager;
|
settingsManagerFactory: (client: Client, appId: string) => SettingsManager;
|
||||||
webhookActivityTogglerFactory: (appId: string, client: Client) => IWebhookActivityTogglerService;
|
webhookActivityTogglerFactory: (appId: string, client: Client) => IWebhookActivityTogglerService;
|
||||||
graphqlClientFactory: (saleorApiUrl: string, token: string) => Client;
|
graphqlClientFactory: (saleorApiUrl: string, token: string) => Client;
|
||||||
};
|
};
|
||||||
|
@ -40,7 +40,7 @@ export const recreateWebhooksHandlerFactory =
|
||||||
logger.debug("Fetching settings");
|
logger.debug("Fetching settings");
|
||||||
const client = graphqlClientFactory(authData.saleorApiUrl, authData.token);
|
const client = graphqlClientFactory(authData.saleorApiUrl, authData.token);
|
||||||
const webhooksToggler = webhookActivityTogglerFactory(authData.appId, client);
|
const webhooksToggler = webhookActivityTogglerFactory(authData.appId, client);
|
||||||
const settingsManager = settingsManagerFactory(client);
|
const settingsManager = settingsManagerFactory(client, authData.appId);
|
||||||
|
|
||||||
const domain = new URL(authData.saleorApiUrl).host;
|
const domain = new URL(authData.saleorApiUrl).host;
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ const logger = createLogger({
|
||||||
* Simple dependency injection - factory injects all services, in tests everything can be configured without mocks
|
* Simple dependency injection - factory injects all services, in tests everything can be configured without mocks
|
||||||
*/
|
*/
|
||||||
type FactoryProps = {
|
type FactoryProps = {
|
||||||
settingsManagerFactory: (client: Client) => SettingsManager;
|
settingsManagerFactory: (client: Client, appId: string) => SettingsManager;
|
||||||
graphqlClientFactory: (saleorApiUrl: string, token: string) => Client;
|
graphqlClientFactory: (saleorApiUrl: string, token: string) => Client;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export const setupIndicesHandlerFactory =
|
||||||
|
|
||||||
logger.debug("Fetching settings");
|
logger.debug("Fetching settings");
|
||||||
const client = graphqlClientFactory(authData.saleorApiUrl, authData.token);
|
const client = graphqlClientFactory(authData.saleorApiUrl, authData.token);
|
||||||
const settingsManager = settingsManagerFactory(client);
|
const settingsManager = settingsManagerFactory(client, authData.appId);
|
||||||
|
|
||||||
const domain = new URL(authData.saleorApiUrl).host;
|
const domain = new URL(authData.saleorApiUrl).host;
|
||||||
|
|
||||||
|
|
8
apps/search/src/pages/api/trpc/[trpc].ts
Normal file
8
apps/search/src/pages/api/trpc/[trpc].ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import * as trpcNext from "@trpc/server/adapters/next";
|
||||||
|
import { createTrpcContext } from "../../../modules/trpc/trpc-context";
|
||||||
|
import { appRouter } from "../../../modules/trpc/trpc-app-router";
|
||||||
|
|
||||||
|
export default trpcNext.createNextApiHandler({
|
||||||
|
router: appRouter,
|
||||||
|
createContext: createTrpcContext,
|
||||||
|
});
|
|
@ -22,10 +22,16 @@ const logger = createLogger({
|
||||||
* Simple dependency injection - factory injects all services, in tests everything can be configured without mocks
|
* Simple dependency injection - factory injects all services, in tests everything can be configured without mocks
|
||||||
*/
|
*/
|
||||||
type FactoryProps = {
|
type FactoryProps = {
|
||||||
settingsManagerFactory: (client: Client) => SettingsManager;
|
settingsManagerFactory: (
|
||||||
webhookActivityTogglerFactory: (appId: string, client: Client) => IWebhookActivityTogglerService;
|
client: Pick<Client, "query" | "mutation">,
|
||||||
|
appId: string,
|
||||||
|
) => SettingsManager;
|
||||||
|
webhookActivityTogglerFactory: (
|
||||||
|
appId: string,
|
||||||
|
client: Pick<Client, "query" | "mutation">,
|
||||||
|
) => IWebhookActivityTogglerService;
|
||||||
algoliaSearchProviderFactory: (appId: string, apiKey: string) => Pick<SearchProvider, "ping">;
|
algoliaSearchProviderFactory: (appId: string, apiKey: string) => Pick<SearchProvider, "ping">;
|
||||||
graphqlClientFactory: (saleorApiUrl: string, token: string) => Client;
|
graphqlClientFactory: (saleorApiUrl: string, token: string) => Pick<Client, "query" | "mutation">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebhooksStatusResponse = {
|
export type WebhooksStatusResponse = {
|
||||||
|
@ -46,7 +52,7 @@ export const webhooksStatusHandlerFactory =
|
||||||
*/
|
*/
|
||||||
const client = graphqlClientFactory(authData.saleorApiUrl, authData.token);
|
const client = graphqlClientFactory(authData.saleorApiUrl, authData.token);
|
||||||
const webhooksToggler = webhookActivityTogglerFactory(authData.appId, client);
|
const webhooksToggler = webhookActivityTogglerFactory(authData.appId, client);
|
||||||
const settingsManager = settingsManagerFactory(client);
|
const settingsManager = settingsManagerFactory(client, authData.appId);
|
||||||
|
|
||||||
const domain = new URL(authData.saleorApiUrl).host;
|
const domain = new URL(authData.saleorApiUrl).host;
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ const mockWebhookContext = {
|
||||||
const appWebhooksResponseData: Pick<OperationResult<FetchOwnWebhooksQuery, any>, "data"> = {
|
const appWebhooksResponseData: Pick<OperationResult<FetchOwnWebhooksQuery, any>, "data"> = {
|
||||||
data: {
|
data: {
|
||||||
app: {
|
app: {
|
||||||
|
id: "appID",
|
||||||
webhooks: [
|
webhooks: [
|
||||||
{
|
{
|
||||||
name: "W1",
|
name: "W1",
|
||||||
|
@ -39,6 +40,7 @@ const appWebhooksResponseData: Pick<OperationResult<FetchOwnWebhooksQuery, any>,
|
||||||
eventDeliveries: {
|
eventDeliveries: {
|
||||||
edges: [],
|
edges: [],
|
||||||
},
|
},
|
||||||
|
targetUrl: "localhost:3000/api/webhooks/test",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
"NEXT_PUBLIC_SENTRY_DSN",
|
"NEXT_PUBLIC_SENTRY_DSN",
|
||||||
"SENTRY_ENVIRONMENT",
|
"SENTRY_ENVIRONMENT",
|
||||||
"APP_IFRAME_BASE_URL",
|
"APP_IFRAME_BASE_URL",
|
||||||
"APP_API_BASE_URL"
|
"APP_API_BASE_URL",
|
||||||
|
"VERCEL_URL",
|
||||||
|
"PORT"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,6 +107,7 @@ async function updatePrivateMetadata(
|
||||||
export class EncryptedMetadataManagerFactory {
|
export class EncryptedMetadataManagerFactory {
|
||||||
constructor(private encryptionKey: string) {
|
constructor(private encryptionKey: string) {
|
||||||
if (!encryptionKey) {
|
if (!encryptionKey) {
|
||||||
|
console.error("Encryption key is required, will throw");
|
||||||
throw new Error("Encryption key is required");
|
throw new Error("Encryption key is required");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
lockfileVersion: '6.0'
|
lockfileVersion: '6.0'
|
||||||
|
|
||||||
settings:
|
|
||||||
autoInstallPeers: true
|
|
||||||
excludeLinksFromLockfile: false
|
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
@ -1053,6 +1049,18 @@ importers:
|
||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: 4.29.19
|
specifier: 4.29.19
|
||||||
version: 4.29.19(react-dom@18.2.0)(react@18.2.0)
|
version: 4.29.19(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@trpc/client':
|
||||||
|
specifier: 10.34.0
|
||||||
|
version: 10.34.0(@trpc/server@10.34.0)
|
||||||
|
'@trpc/next':
|
||||||
|
specifier: 10.34.0
|
||||||
|
version: 10.34.0(@tanstack/react-query@4.29.19)(@trpc/client@10.34.0)(@trpc/react-query@10.34.0)(@trpc/server@10.34.0)(next@13.4.8)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@trpc/react-query':
|
||||||
|
specifier: 10.34.0
|
||||||
|
version: 10.34.0(@tanstack/react-query@4.29.19)(@trpc/client@10.34.0)(@trpc/server@10.34.0)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@trpc/server':
|
||||||
|
specifier: 10.34.0
|
||||||
|
version: 10.34.0
|
||||||
'@types/debug':
|
'@types/debug':
|
||||||
specifier: ^4.1.7
|
specifier: ^4.1.7
|
||||||
version: 4.1.8
|
version: 4.1.8
|
||||||
|
@ -5661,6 +5669,7 @@ packages:
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.14.0
|
regenerator-runtime: 0.14.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@babel/runtime@7.22.11:
|
/@babel/runtime@7.22.11:
|
||||||
resolution: {integrity: sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==}
|
resolution: {integrity: sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==}
|
||||||
|
@ -5794,7 +5803,7 @@ packages:
|
||||||
/@changesets/apply-release-plan@6.1.4:
|
/@changesets/apply-release-plan@6.1.4:
|
||||||
resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==}
|
resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.22.10
|
'@babel/runtime': 7.22.11
|
||||||
'@changesets/config': 2.3.1
|
'@changesets/config': 2.3.1
|
||||||
'@changesets/get-version-range-type': 0.3.2
|
'@changesets/get-version-range-type': 0.3.2
|
||||||
'@changesets/git': 2.0.0
|
'@changesets/git': 2.0.0
|
||||||
|
@ -5811,7 +5820,7 @@ packages:
|
||||||
/@changesets/assemble-release-plan@5.2.4:
|
/@changesets/assemble-release-plan@5.2.4:
|
||||||
resolution: {integrity: sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==}
|
resolution: {integrity: sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.22.10
|
'@babel/runtime': 7.22.11
|
||||||
'@changesets/errors': 0.1.4
|
'@changesets/errors': 0.1.4
|
||||||
'@changesets/get-dependents-graph': 1.3.6
|
'@changesets/get-dependents-graph': 1.3.6
|
||||||
'@changesets/types': 5.2.1
|
'@changesets/types': 5.2.1
|
||||||
|
@ -5889,7 +5898,7 @@ packages:
|
||||||
/@changesets/get-release-plan@3.0.17:
|
/@changesets/get-release-plan@3.0.17:
|
||||||
resolution: {integrity: sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==}
|
resolution: {integrity: sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.22.10
|
'@babel/runtime': 7.22.11
|
||||||
'@changesets/assemble-release-plan': 5.2.4
|
'@changesets/assemble-release-plan': 5.2.4
|
||||||
'@changesets/config': 2.3.1
|
'@changesets/config': 2.3.1
|
||||||
'@changesets/pre': 1.0.14
|
'@changesets/pre': 1.0.14
|
||||||
|
@ -5903,7 +5912,7 @@ packages:
|
||||||
/@changesets/git@2.0.0:
|
/@changesets/git@2.0.0:
|
||||||
resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==}
|
resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.22.10
|
'@babel/runtime': 7.22.11
|
||||||
'@changesets/errors': 0.1.4
|
'@changesets/errors': 0.1.4
|
||||||
'@changesets/types': 5.2.1
|
'@changesets/types': 5.2.1
|
||||||
'@manypkg/get-packages': 1.1.3
|
'@manypkg/get-packages': 1.1.3
|
||||||
|
@ -5925,7 +5934,7 @@ packages:
|
||||||
/@changesets/pre@1.0.14:
|
/@changesets/pre@1.0.14:
|
||||||
resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==}
|
resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.22.10
|
'@babel/runtime': 7.22.11
|
||||||
'@changesets/errors': 0.1.4
|
'@changesets/errors': 0.1.4
|
||||||
'@changesets/types': 5.2.1
|
'@changesets/types': 5.2.1
|
||||||
'@manypkg/get-packages': 1.1.3
|
'@manypkg/get-packages': 1.1.3
|
||||||
|
@ -5934,7 +5943,7 @@ packages:
|
||||||
/@changesets/read@0.5.9:
|
/@changesets/read@0.5.9:
|
||||||
resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==}
|
resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.22.10
|
'@babel/runtime': 7.22.11
|
||||||
'@changesets/git': 2.0.0
|
'@changesets/git': 2.0.0
|
||||||
'@changesets/logger': 0.0.5
|
'@changesets/logger': 0.0.5
|
||||||
'@changesets/parse': 0.3.16
|
'@changesets/parse': 0.3.16
|
||||||
|
@ -5952,7 +5961,7 @@ packages:
|
||||||
/@changesets/write@0.2.3:
|
/@changesets/write@0.2.3:
|
||||||
resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==}
|
resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.22.10
|
'@babel/runtime': 7.22.11
|
||||||
'@changesets/types': 5.2.1
|
'@changesets/types': 5.2.1
|
||||||
fs-extra: 7.0.1
|
fs-extra: 7.0.1
|
||||||
human-id: 1.0.2
|
human-id: 1.0.2
|
||||||
|
@ -7693,7 +7702,7 @@ packages:
|
||||||
/@manypkg/get-packages@1.1.3:
|
/@manypkg/get-packages@1.1.3:
|
||||||
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
|
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.22.10
|
'@babel/runtime': 7.22.11
|
||||||
'@changesets/types': 4.1.0
|
'@changesets/types': 4.1.0
|
||||||
'@manypkg/find-root': 1.1.0
|
'@manypkg/find-root': 1.1.0
|
||||||
fs-extra: 8.1.0
|
fs-extra: 8.1.0
|
||||||
|
@ -21366,3 +21375,7 @@ packages:
|
||||||
|
|
||||||
/zod@3.21.4:
|
/zod@3.21.4:
|
||||||
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
|
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
Loading…
Reference in a new issue