add metadata manager to shared package (#901)
This commit is contained in:
parent
8b3d9617e6
commit
c50797e836
11 changed files with 282 additions and 474 deletions
6
.changeset/great-mice-lie.md
Normal file
6
.changeset/great-mice-lie.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
"saleor-app-segment": patch
|
||||||
|
"saleor-app-cms-v2": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Extracted MetadataManager creation to factory from shared package
|
5
.changeset/itchy-games-hear.md
Normal file
5
.changeset/itchy-games-hear.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@saleor/apps-shared": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added Metadata Manager factory that abstract creation of EncryptedMetadataManager from SDK
|
|
@ -1,101 +1,12 @@
|
||||||
import {
|
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||||
MetadataEntry,
|
import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared";
|
||||||
EncryptedMetadataManager,
|
import { Client } from "urql";
|
||||||
SettingsManager,
|
|
||||||
} from "@saleor/app-sdk/settings-manager";
|
|
||||||
import { Client, gql } from "urql";
|
|
||||||
import {
|
|
||||||
FetchAppDetailsDocument,
|
|
||||||
FetchAppDetailsQuery,
|
|
||||||
UpdateAppMetadataDocument,
|
|
||||||
} from "../../../generated/graphql";
|
|
||||||
|
|
||||||
gql`
|
const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!);
|
||||||
mutation UpdateAppMetadata($id: ID!, $input: [MetadataInput!]!) {
|
|
||||||
updatePrivateMetadata(id: $id, input: $input) {
|
|
||||||
item {
|
|
||||||
privateMetadata {
|
|
||||||
key
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
gql`
|
|
||||||
query FetchAppDetails {
|
|
||||||
app {
|
|
||||||
id
|
|
||||||
privateMetadata {
|
|
||||||
key
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
gql`
|
|
||||||
mutation RemoveMetadata($id: ID!, $keys: [String!]!) {
|
|
||||||
deletePrivateMetadata(id: $id, keys: $keys) {
|
|
||||||
errors {
|
|
||||||
message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type SimpleGraphqlClient = Pick<Client, "mutation" | "query">;
|
|
||||||
|
|
||||||
async function fetchAllMetadata(client: SimpleGraphqlClient): Promise<MetadataEntry[]> {
|
|
||||||
const { error, data } = await client
|
|
||||||
.query<FetchAppDetailsQuery>(FetchAppDetailsDocument, {})
|
|
||||||
.toPromise();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data?.app?.privateMetadata.map((md) => ({ key: md.key, value: md.value })) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function mutateMetadata(
|
|
||||||
client: SimpleGraphqlClient,
|
|
||||||
metadata: MetadataEntry[],
|
|
||||||
appId: string
|
|
||||||
) {
|
|
||||||
const { error: mutationError, data: mutationData } = await client
|
|
||||||
.mutation(UpdateAppMetadataDocument, {
|
|
||||||
id: appId,
|
|
||||||
input: metadata,
|
|
||||||
})
|
|
||||||
.toPromise();
|
|
||||||
|
|
||||||
if (mutationError) {
|
|
||||||
throw new Error(`Mutation error: ${mutationError.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
mutationData?.updatePrivateMetadata?.item?.privateMetadata.map((md) => ({
|
|
||||||
key: md.key,
|
|
||||||
value: md.value,
|
|
||||||
})) || []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createSettingsManager = (
|
export const createSettingsManager = (
|
||||||
client: SimpleGraphqlClient,
|
client: Pick<Client, "query" | "mutation">,
|
||||||
appId: string
|
appId: string,
|
||||||
): SettingsManager => {
|
): SettingsManager => {
|
||||||
/*
|
return metadataManagerFactory.create(client, appId);
|
||||||
* 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: process.env.SECRET_KEY!,
|
|
||||||
fetchMetadata: () => fetchAllMetadata(client),
|
|
||||||
mutateMetadata: (metadata) => mutateMetadata(client, metadata, appId),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
export {};
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
vi.stubEnv("SECRET_KEY", "TEST");
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { SimpleGraphqlClient } from "../metadata-manager";
|
||||||
export const mockMetadataManager = {
|
export const mockMetadataManager = {
|
||||||
set: vi.fn().mockImplementation(async () => {}),
|
set: vi.fn().mockImplementation(async () => {}),
|
||||||
get: vi.fn().mockImplementation(async () => {}),
|
get: vi.fn().mockImplementation(async () => {}),
|
||||||
|
delete: vi.fn().mockImplementation(async () => {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createSettingsManager = (client: SimpleGraphqlClient): SettingsManager => {
|
export const createSettingsManager = (client: SimpleGraphqlClient): SettingsManager => {
|
||||||
|
|
|
@ -30,6 +30,7 @@ const appWebhooksResponseData: Pick<OperationResult<FetchOwnWebhooksQuery, any>,
|
||||||
app: {
|
app: {
|
||||||
webhooks: [
|
webhooks: [
|
||||||
{
|
{
|
||||||
|
name: "W1",
|
||||||
id: "w1",
|
id: "w1",
|
||||||
isActive: true,
|
isActive: true,
|
||||||
asyncEvents: [
|
asyncEvents: [
|
||||||
|
@ -53,6 +54,7 @@ describe("webhooksStatusHandler", () => {
|
||||||
const webhooksTogglerServiceMock: IWebhookActivityTogglerService = {
|
const webhooksTogglerServiceMock: IWebhookActivityTogglerService = {
|
||||||
disableOwnWebhooks: vi.fn(),
|
disableOwnWebhooks: vi.fn(),
|
||||||
enableOwnWebhooks: vi.fn(),
|
enableOwnWebhooks: vi.fn(),
|
||||||
|
recreateOwnWebhooks: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const algoliaSearchProviderMock: Pick<SearchProvider, "ping"> = {
|
const algoliaSearchProviderMock: Pick<SearchProvider, "ping"> = {
|
||||||
|
@ -62,6 +64,7 @@ describe("webhooksStatusHandler", () => {
|
||||||
const settingsManagerMock: SettingsManager = {
|
const settingsManagerMock: SettingsManager = {
|
||||||
get: vi.fn(),
|
get: vi.fn(),
|
||||||
set: vi.fn(),
|
set: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let handler: NextProtectedApiHandler;
|
let handler: NextProtectedApiHandler;
|
||||||
|
|
|
@ -1,101 +1,12 @@
|
||||||
import {
|
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||||
MetadataEntry,
|
import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared";
|
||||||
EncryptedMetadataManager,
|
import { Client } from "urql";
|
||||||
SettingsManager,
|
|
||||||
} from "@saleor/app-sdk/settings-manager";
|
|
||||||
import { Client, gql } from "urql";
|
|
||||||
import {
|
|
||||||
FetchAppDetailsDocument,
|
|
||||||
FetchAppDetailsQuery,
|
|
||||||
UpdateAppMetadataDocument,
|
|
||||||
} from "../../../generated/graphql";
|
|
||||||
|
|
||||||
gql`
|
const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!);
|
||||||
mutation UpdateAppMetadata($id: ID!, $input: [MetadataInput!]!) {
|
|
||||||
updatePrivateMetadata(id: $id, input: $input) {
|
|
||||||
item {
|
|
||||||
privateMetadata {
|
|
||||||
key
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
gql`
|
|
||||||
query FetchAppDetails {
|
|
||||||
app {
|
|
||||||
id
|
|
||||||
privateMetadata {
|
|
||||||
key
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
gql`
|
|
||||||
mutation RemoveMetadata($id: ID!, $keys: [String!]!) {
|
|
||||||
deletePrivateMetadata(id: $id, keys: $keys) {
|
|
||||||
errors {
|
|
||||||
message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type SimpleGraphqlClient = Pick<Client, "mutation" | "query">;
|
|
||||||
|
|
||||||
async function fetchAllMetadata(client: SimpleGraphqlClient): Promise<MetadataEntry[]> {
|
|
||||||
const { error, data } = await client
|
|
||||||
.query<FetchAppDetailsQuery>(FetchAppDetailsDocument, {})
|
|
||||||
.toPromise();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data?.app?.privateMetadata.map((md) => ({ key: md.key, value: md.value })) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function mutateMetadata(
|
|
||||||
client: SimpleGraphqlClient,
|
|
||||||
metadata: MetadataEntry[],
|
|
||||||
appId: string,
|
|
||||||
) {
|
|
||||||
const { error: mutationError, data: mutationData } = await client
|
|
||||||
.mutation(UpdateAppMetadataDocument, {
|
|
||||||
id: appId,
|
|
||||||
input: metadata,
|
|
||||||
})
|
|
||||||
.toPromise();
|
|
||||||
|
|
||||||
if (mutationError) {
|
|
||||||
throw new Error(`Mutation error: ${mutationError.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
mutationData?.updatePrivateMetadata?.item?.privateMetadata.map((md) => ({
|
|
||||||
key: md.key,
|
|
||||||
value: md.value,
|
|
||||||
})) || []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createSettingsManager = (
|
export const createSettingsManager = (
|
||||||
client: SimpleGraphqlClient,
|
client: Pick<Client, "query" | "mutation">,
|
||||||
appId: string,
|
appId: string,
|
||||||
): SettingsManager => {
|
): SettingsManager => {
|
||||||
/*
|
return metadataManagerFactory.create(client, appId);
|
||||||
* 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: process.env.SECRET_KEY!,
|
|
||||||
fetchMetadata: () => fetchAllMetadata(client),
|
|
||||||
mutateMetadata: (metadata) => mutateMetadata(client, metadata, appId),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,3 +5,4 @@ export * from "./src/use-dashboard-notification";
|
||||||
export * from "./src/logger";
|
export * from "./src/logger";
|
||||||
export * from "./src/saleor-version-compatibility-validator";
|
export * from "./src/saleor-version-compatibility-validator";
|
||||||
export * from "./src/create-graphql-client";
|
export * from "./src/create-graphql-client";
|
||||||
|
export * from "./src/metadata-manager";
|
||||||
|
|
|
@ -9,9 +9,7 @@
|
||||||
"eslint": "8.46.0",
|
"eslint": "8.46.0",
|
||||||
"pino": "^8.14.1",
|
"pino": "^8.14.1",
|
||||||
"pino-pretty": "^10.0.0",
|
"pino-pretty": "^10.0.0",
|
||||||
"semver": "^7.5.1",
|
"semver": "^7.5.1"
|
||||||
"typescript": "5.1.6",
|
|
||||||
"urql": "^4.0.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@material-ui/core": "^4.12.4",
|
"@material-ui/core": "^4.12.4",
|
||||||
|
@ -27,6 +25,8 @@
|
||||||
"next": "13.4.8",
|
"next": "13.4.8",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"typescript": "5.1.6",
|
||||||
|
"urql": "^4.0.4",
|
||||||
"vite": "4.4.8",
|
"vite": "4.4.8",
|
||||||
"vitest": "0.34.1"
|
"vitest": "0.34.1"
|
||||||
},
|
},
|
||||||
|
@ -37,6 +37,7 @@
|
||||||
"pino": "^8.14.1",
|
"pino": "^8.14.1",
|
||||||
"pino-pretty": "^10.0.0",
|
"pino-pretty": "^10.0.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0"
|
"react-dom": "18.2.0",
|
||||||
|
"urql": "^4.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
127
packages/shared/src/metadata-manager.ts
Normal file
127
packages/shared/src/metadata-manager.ts
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import {
|
||||||
|
MetadataEntry,
|
||||||
|
EncryptedMetadataManager,
|
||||||
|
MetadataManager,
|
||||||
|
SettingsManager,
|
||||||
|
} from "@saleor/app-sdk/settings-manager";
|
||||||
|
import { Client, gql } from "urql";
|
||||||
|
|
||||||
|
const UpdateAppMetadataMutation = gql`
|
||||||
|
mutation UpdateAppMetadata($id: ID!, $input: [MetadataInput!]!) {
|
||||||
|
updatePrivateMetadata(id: $id, input: $input) {
|
||||||
|
item {
|
||||||
|
privateMetadata {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FetchAppDetailsQuery = gql`
|
||||||
|
query FetchAppDetails {
|
||||||
|
app {
|
||||||
|
id
|
||||||
|
privateMetadata {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DeletePrivateMetadataMutation = gql`
|
||||||
|
mutation RemovePrivateMetadata($id: ID!, $keys: [String!]!) {
|
||||||
|
deletePrivateMetadata(id: $id, keys: $keys) {
|
||||||
|
errors {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To avoid a graphql-schema build step, manually set types for the queries and mutations.
|
||||||
|
*/
|
||||||
|
type FetchAppPrivateMetadataQuery = {
|
||||||
|
__typename?: "Query";
|
||||||
|
app?: {
|
||||||
|
__typename?: "App";
|
||||||
|
id: string;
|
||||||
|
privateMetadata: Array<{ __typename?: "MetadataItem"; key: string; value: string }>;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdateAppPrivateMetadataMutation = {
|
||||||
|
__typename?: "Mutation";
|
||||||
|
updatePrivateMetadata?: {
|
||||||
|
__typename?: "UpdatePrivateMetadata";
|
||||||
|
item?: {
|
||||||
|
__typename?: "App";
|
||||||
|
privateMetadata: Array<{ __typename?: "MetadataItem"; key: string; value: string }>;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MetadataManagerGraphqlClient = Pick<Client, "mutation" | "query">;
|
||||||
|
|
||||||
|
async function fetchAllPrivateMetadata(
|
||||||
|
client: MetadataManagerGraphqlClient,
|
||||||
|
): Promise<MetadataEntry[]> {
|
||||||
|
const { error, data } = await client
|
||||||
|
.query<FetchAppPrivateMetadataQuery>(FetchAppDetailsQuery, {})
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data?.app?.privateMetadata.map((md) => ({ key: md.key, value: md.value })) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePrivateMetadata(
|
||||||
|
client: MetadataManagerGraphqlClient,
|
||||||
|
metadata: MetadataEntry[],
|
||||||
|
appId: string,
|
||||||
|
) {
|
||||||
|
const { error: mutationError, data: mutationData } = await client
|
||||||
|
.mutation<UpdateAppPrivateMetadataMutation>(UpdateAppMetadataMutation, {
|
||||||
|
id: appId,
|
||||||
|
input: metadata,
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
if (mutationError) {
|
||||||
|
throw new Error(`Mutation error: ${mutationError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
mutationData?.updatePrivateMetadata?.item?.privateMetadata.map((md) => ({
|
||||||
|
key: md.key,
|
||||||
|
value: md.value,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EncryptedMetadataManagerFactory {
|
||||||
|
constructor(private encryptionKey: string) {
|
||||||
|
if (!encryptionKey) {
|
||||||
|
throw new Error("Encryption key is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
create(client: MetadataManagerGraphqlClient, appId: string): SettingsManager {
|
||||||
|
return new EncryptedMetadataManager({
|
||||||
|
encryptionKey: this.encryptionKey,
|
||||||
|
fetchMetadata: () => fetchAllPrivateMetadata(client),
|
||||||
|
mutateMetadata: (metadata) => updatePrivateMetadata(client, metadata, appId),
|
||||||
|
async deleteMetadata(keys) {
|
||||||
|
await client.mutation(DeletePrivateMetadataMutation, {
|
||||||
|
id: appId,
|
||||||
|
keys: keys,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
396
pnpm-lock.yaml
396
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue