add metadata manager to shared package (#901)

This commit is contained in:
Lukasz Ostrowski 2023-08-17 11:53:03 +02:00 committed by GitHub
parent 8b3d9617e6
commit c50797e836
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 282 additions and 474 deletions

View file

@ -0,0 +1,6 @@
---
"saleor-app-segment": patch
"saleor-app-cms-v2": patch
---
Extracted MetadataManager creation to factory from shared package

View file

@ -0,0 +1,5 @@
---
"@saleor/apps-shared": minor
---
Added Metadata Manager factory that abstract creation of EncryptedMetadataManager from SDK

View file

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

View file

@ -1 +1,3 @@
export {}; import { vi } from "vitest";
vi.stubEnv("SECRET_KEY", "TEST");

View file

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

View file

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

View file

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

View file

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

View file

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

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

File diff suppressed because it is too large Load diff