From 092b9292d088e80b2fd35f7d0f79e2a6169600de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:03:51 +0100 Subject: [PATCH 1/8] Add DynamoDB APL --- apps/segment/README.md | 47 ++++++ apps/segment/docker-compose.yml | 12 ++ apps/segment/package.json | 4 + apps/segment/scripts/setup-dynamodb.sh | 11 ++ apps/segment/src/env.ts | 10 +- apps/segment/src/lib/dynamodb-apl.ts | 107 ++++++++++++++ apps/segment/src/lib/dynamodb-client.ts | 12 ++ apps/segment/src/logger.ts | 3 + .../src/modules/db/segment-apl-mapper.ts | 28 ++++ .../src/modules/db/segment-apl-repository.ts | 134 ++++++++++++++++++ .../src/modules/db/segment-main-table.ts | 90 ++++++++++++ apps/segment/src/saleor-app.ts | 29 ++++ apps/segment/turbo.json | 6 +- 13 files changed, 491 insertions(+), 2 deletions(-) create mode 100644 apps/segment/docker-compose.yml create mode 100755 apps/segment/scripts/setup-dynamodb.sh create mode 100644 apps/segment/src/lib/dynamodb-apl.ts create mode 100644 apps/segment/src/lib/dynamodb-client.ts create mode 100644 apps/segment/src/modules/db/segment-apl-mapper.ts create mode 100644 apps/segment/src/modules/db/segment-apl-repository.ts create mode 100644 apps/segment/src/modules/db/segment-main-table.ts diff --git a/apps/segment/README.md b/apps/segment/README.md index 5d5513f6a..c7fa9c354 100644 --- a/apps/segment/README.md +++ b/apps/segment/README.md @@ -62,3 +62,50 @@ To start the migration run command: ```bash pnpm migrate ``` + +### Setting up DynamoDB + +Segment app uses DynamoDB as it's internal database. + +In order to work properly you need to either set-up local DynamoDB instance or connect to a real DynamoDB on AWS account. + +#### Local DynamoDB + +To use a local DynamoDB instance you can use Docker Compose: + +```bash +docker compose up +``` + +After that a local DynamoDB instance will be spun-up at `http://localhost:8000`. + +To set up tables needed for the app run following command for each table used in app: + +```shell +./scripts/setup-dynamodb.sh +``` + +After setting up database, you must configure following variables: + +```bash +DYNAMODB_MAIN_TABLE_NAME=segment-main-table +AWS_REGION=localhost +AWS_ENDPOINT_URL=http://localhost:8000 +AWS_ACCESS_KEY_ID=fake_id +AWS_SECRET_ACCESS_KEY=fake_key +``` + +Local instance doesn't require providing any authentication details. + +To see data stored by the app you can use [NoSQL Workbench](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.html) app provided by AWS. After installing the app go to Operation builder > Add connection > DynamoDB local and use the default values. + +#### Production DynamoDB + +To configure DynamoDB for production usage, provide credentials in a default AWS SDK format (see [AWS Docs](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html)) + +```bash +DYNAMODB_MAIN_TABLE_NAME=segment-main-table +AWS_REGION=us-east-1 # Region when DynamoDB was deployed +AWS_ACCESS_KEY_ID=AK... +AWS_SECRET_ACCESS_KEY=... +``` diff --git a/apps/segment/docker-compose.yml b/apps/segment/docker-compose.yml new file mode 100644 index 000000000..815e6fc34 --- /dev/null +++ b/apps/segment/docker-compose.yml @@ -0,0 +1,12 @@ +services: + dynamodb: + command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" + image: "amazon/dynamodb-local:latest" + ports: + - "8000:8000" + volumes: + - "./docker/dynamodb:/home/dynamodblocal/data" + working_dir: /home/dynamodblocal +volumes: + dynamodb: + driver: local diff --git a/apps/segment/package.json b/apps/segment/package.json index ad4e900f3..d46484791 100644 --- a/apps/segment/package.json +++ b/apps/segment/package.json @@ -16,6 +16,10 @@ "test": "vitest" }, "dependencies": { + "@aws-sdk/client-dynamodb": "3.651.1", + "@aws-sdk/lib-dynamodb": "3.651.1", + "@aws-sdk/util-dynamodb": "3.651.1", + "dynamodb-toolbox": "1.8.2", "@hookform/resolvers": "^3.3.1", "@opentelemetry/api": "../../node_modules/@opentelemetry/api", "@opentelemetry/api-logs": "../../node_modules/@opentelemetry/api-logs", diff --git a/apps/segment/scripts/setup-dynamodb.sh b/apps/segment/scripts/setup-dynamodb.sh new file mode 100755 index 000000000..c22910f50 --- /dev/null +++ b/apps/segment/scripts/setup-dynamodb.sh @@ -0,0 +1,11 @@ +#!/bin/bash +if ! aws dynamodb describe-table --table-name segment-main-table --endpoint-url http://localhost:8000 --region localhost >/dev/null 2>&1; then + aws dynamodb create-table --table-name segment-main-table \ + --attribute-definitions AttributeName=PK,AttributeType=S AttributeName=SK,AttributeType=S \ + --key-schema AttributeName=PK,KeyType=HASH AttributeName=SK,KeyType=RANGE \ + --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \ + --endpoint-url http://localhost:8000 \ + --region localhost +else + echo "Table segment-main-table already exists - creation is skipped" +fi diff --git a/apps/segment/src/env.ts b/apps/segment/src/env.ts index d1080fb6a..42df8546c 100644 --- a/apps/segment/src/env.ts +++ b/apps/segment/src/env.ts @@ -14,7 +14,7 @@ export const env = createEnv({ }, server: { ALLOWED_DOMAIN_PATTERN: z.string().optional(), - APL: z.enum(["saleor-cloud", "file"]).optional().default("file"), + APL: z.enum(["saleor-cloud", "file", "dynamodb"]).optional().default("file"), APP_API_BASE_URL: z.string().optional(), APP_IFRAME_BASE_URL: z.string().optional(), APP_LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info"), @@ -27,6 +27,10 @@ export const env = createEnv({ PORT: z.coerce.number().optional().default(3000), SECRET_KEY: z.string(), VERCEL_URL: z.string().optional(), + DYNAMODB_MAIN_TABLE_NAME: z.string().optional(), + AWS_REGION: z.string().optional(), + AWS_ACCESS_KEY_ID: z.string().optional(), + AWS_SECRET_ACCESS_KEY: z.string().optional(), }, shared: { NODE_ENV: z.enum(["development", "production", "test"]).optional().default("development"), @@ -51,6 +55,10 @@ export const env = createEnv({ REST_APL_TOKEN: process.env.REST_APL_TOKEN, SECRET_KEY: process.env.SECRET_KEY, VERCEL_URL: process.env.VERCEL_URL, + DYNAMODB_MAIN_TABLE_NAME: process.env.DYNAMODB_MAIN_TABLE_NAME, + AWS_REGION: process.env.AWS_REGION, + AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY, }, isServer: typeof window === "undefined" || process.env.NODE_ENV === "test", }); diff --git a/apps/segment/src/lib/dynamodb-apl.ts b/apps/segment/src/lib/dynamodb-apl.ts new file mode 100644 index 000000000..14f2a36e9 --- /dev/null +++ b/apps/segment/src/lib/dynamodb-apl.ts @@ -0,0 +1,107 @@ +import { APL, AplConfiguredResult, AplReadyResult, AuthData } from "@saleor/app-sdk/APL"; + +import { BaseError } from "@/errors"; +import { SegmentAPLRepository } from "@/modules/db/segment-apl-repository"; +import { SegmentAPLEntityType } from "@/modules/db/segment-main-table"; + +export class DynamoAPL implements APL { + private segmentAPLRepository: SegmentAPLRepository; + + static SetAuthDataError = BaseError.subclass("SetAuthDataError"); + static DeleteAuthDataError = BaseError.subclass("DeleteAuthDataError"); + static MissingEnvVariablesError = BaseError.subclass("MissingEnvVariablesError"); + + constructor({ segmentAPLEntity }: { segmentAPLEntity: SegmentAPLEntityType }) { + this.segmentAPLRepository = new SegmentAPLRepository({ segmentAPLEntity }); + } + + async get(saleorApiUrl: string): Promise { + const getEntryResult = await this.segmentAPLRepository.getEntry({ + saleorApiUrl, + }); + + if (getEntryResult.isErr()) { + // TODO: should we throw here? + return undefined; + } + + return getEntryResult.value; + } + + async set(authData: AuthData): Promise { + const setEntryResult = await this.segmentAPLRepository.setEntry({ + authData, + }); + + if (setEntryResult.isErr()) { + throw new DynamoAPL.SetAuthDataError("Failed to set APL entry", { + cause: setEntryResult.error, + }); + } + + return undefined; + } + + async delete(saleorApiUrl: string): Promise { + const deleteEntryResult = await this.segmentAPLRepository.deleteEntry({ + saleorApiUrl, + }); + + if (deleteEntryResult.isErr()) { + throw new DynamoAPL.DeleteAuthDataError("Failed to delete APL entry", { + cause: deleteEntryResult.error, + }); + } + + return undefined; + } + + async getAll(): Promise { + const getAllEntriesResult = await this.segmentAPLRepository.getAllEntries(); + + if (getAllEntriesResult.isErr()) { + // TODO: should we throw here? + return []; + } + + return getAllEntriesResult.value; + } + + async isReady(): Promise { + const ready = this.envVariablesRequriedByDynamoDBExist(); + + return ready + ? { + ready: true, + } + : { + ready: false, + error: new DynamoAPL.MissingEnvVariablesError("Missing DynamoDB env variables"), + }; + } + + async isConfigured(): Promise { + const configured = this.envVariablesRequriedByDynamoDBExist(); + + return configured + ? { + configured: true, + } + : { + configured: false, + error: new DynamoAPL.MissingEnvVariablesError("Missing DynamoDB env variables"), + }; + } + + private envVariablesRequriedByDynamoDBExist() { + const variables = [ + "DYNAMODB_MAIN_TABLE_NAME", + "AWS_REGION", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + ]; + + // eslint-disable-next-line node/no-process-env + return variables.every((variable) => !!process.env[variable]); + } +} diff --git a/apps/segment/src/lib/dynamodb-client.ts b/apps/segment/src/lib/dynamodb-client.ts new file mode 100644 index 000000000..b5d56ee28 --- /dev/null +++ b/apps/segment/src/lib/dynamodb-client.ts @@ -0,0 +1,12 @@ +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; + +export const createDynamoDBClient = () => { + const client = new DynamoDBClient(); + + return client; +}; + +export const createDynamoDBDocumentClient = (client: DynamoDBClient) => { + return DynamoDBDocumentClient.from(client); +}; diff --git a/apps/segment/src/logger.ts b/apps/segment/src/logger.ts index 95a2ecb92..0565fa74f 100644 --- a/apps/segment/src/logger.ts +++ b/apps/segment/src/logger.ts @@ -1,10 +1,13 @@ import { attachLoggerConsoleTransport, rootLogger } from "@saleor/apps-logger"; +import { createRequire } from "module"; import packageJson from "../package.json"; import { env } from "./env"; rootLogger.settings.maskValuesOfKeys = ["metadata", "username", "password", "apiKey"]; +const require = createRequire(import.meta.url); + if (env.NODE_ENV !== "production") { attachLoggerConsoleTransport(rootLogger); } diff --git a/apps/segment/src/modules/db/segment-apl-mapper.ts b/apps/segment/src/modules/db/segment-apl-mapper.ts new file mode 100644 index 000000000..9e613aa8a --- /dev/null +++ b/apps/segment/src/modules/db/segment-apl-mapper.ts @@ -0,0 +1,28 @@ +import { AuthData } from "@saleor/app-sdk/APL"; +import { FormattedItem, type PutItemInput } from "dynamodb-toolbox"; + +import { SegmentAPLEntityType, SegmentMainTable } from "@/modules/db/segment-main-table"; + +export class SegmentAPLMapper { + dynamoDBEntityToAuthData(entity: FormattedItem): AuthData { + return { + domain: entity.domain, + token: entity.token, + saleorApiUrl: entity.saleorApiUrl, + appId: entity.appId, + jwks: entity.jwks, + }; + } + + authDataToDynamoPutEntity(authData: AuthData): PutItemInput { + return { + PK: SegmentMainTable.getAPLPrimaryKey({ saleorApiUrl: authData.saleorApiUrl }), + SK: SegmentMainTable.getAPLSortKey(), + domain: authData.domain, + token: authData.token, + saleorApiUrl: authData.saleorApiUrl, + appId: authData.appId, + jwks: authData.jwks, + }; + } +} diff --git a/apps/segment/src/modules/db/segment-apl-repository.ts b/apps/segment/src/modules/db/segment-apl-repository.ts new file mode 100644 index 000000000..4a245c453 --- /dev/null +++ b/apps/segment/src/modules/db/segment-apl-repository.ts @@ -0,0 +1,134 @@ +import { AuthData } from "@saleor/app-sdk/APL"; +import { DeleteItemCommand, GetItemCommand, PutItemCommand, ScanCommand } from "dynamodb-toolbox"; +import { err, ok, ResultAsync } from "neverthrow"; + +import { BaseError } from "@/errors"; +import { createLogger } from "@/logger"; +import { SegmentAPLEntityType, SegmentMainTable } from "@/modules/db/segment-main-table"; + +import { SegmentAPLMapper } from "./segment-apl-mapper"; + +export class SegmentAPLRepository { + private logger = createLogger("DynamoDBAPLRepository"); + + private segmentAPLMapper = new SegmentAPLMapper(); + + static ReadEntityError = BaseError.subclass("ReadEntityError"); + static WriteEntityError = BaseError.subclass("WriteEntityError"); + static DeleteEntityError = BaseError.subclass("DeleteEntityError"); + static ScanEntityError = BaseError.subclass("ScanEntityError"); + + constructor( + private deps: { + segmentAPLEntity: SegmentAPLEntityType; + }, + ) {} + + async getEntry(args: { saleorApiUrl: string }) { + const getEntryResult = await ResultAsync.fromPromise( + this.deps.segmentAPLEntity + .build(GetItemCommand) + .key({ + PK: SegmentMainTable.getAPLPrimaryKey({ + saleorApiUrl: args.saleorApiUrl, + }), + SK: SegmentMainTable.getAPLSortKey(), + }) + .send(), + (error) => + new SegmentAPLRepository.ReadEntityError("Failed to read APL entity", { cause: error }), + ); + + if (getEntryResult.isErr()) { + this.logger.error("Error while reading APL entity from DynamoDB", { + error: getEntryResult.error, + }); + + return err(getEntryResult.error); + } + + if (!getEntryResult.value.Item) { + this.logger.warn("APL entry not found", { args }); + + return err(new SegmentAPLRepository.ReadEntityError("APL entry not found")); + } + + return ok(this.segmentAPLMapper.dynamoDBEntityToAuthData(getEntryResult.value.Item)); + } + + async setEntry(args: { authData: AuthData }) { + const setEntryResult = await ResultAsync.fromPromise( + this.deps.segmentAPLEntity + .build(PutItemCommand) + .item(this.segmentAPLMapper.authDataToDynamoPutEntity(args.authData)) + .send(), + (error) => + new SegmentAPLRepository.WriteEntityError("Failed to write APL entity", { cause: error }), + ); + + if (setEntryResult.isErr()) { + this.logger.error("Error while putting APL into DynamoDB", { + error: setEntryResult.error, + }); + + return err(setEntryResult.error); + } + + return ok(undefined); + } + + async deleteEntry(args: { saleorApiUrl: string }) { + const deleteEntryResult = await ResultAsync.fromPromise( + this.deps.segmentAPLEntity + .build(DeleteItemCommand) + .key({ + PK: SegmentMainTable.getAPLPrimaryKey({ + saleorApiUrl: args.saleorApiUrl, + }), + SK: SegmentMainTable.getAPLSortKey(), + }) + .send(), + (error) => + new SegmentAPLRepository.DeleteEntityError("Failed to delete APL entity", { + cause: error, + }), + ); + + if (deleteEntryResult.isErr()) { + this.logger.error("Error while deleting entry APL from DynamoDB", { + error: deleteEntryResult.error, + }); + + return err(deleteEntryResult.error); + } + + return ok(undefined); + } + + async getAllEntries() { + const scanEntriesResult = await ResultAsync.fromPromise( + this.deps.segmentAPLEntity.table + .build(ScanCommand) + .entities(this.deps.segmentAPLEntity) + .options({ + // keep all the entries in memory - we should introduce pagination in the future + maxPages: Infinity, + }) + .send(), + (error) => + new SegmentAPLRepository.ScanEntityError("Failed to scan APL entities", { cause: error }), + ); + + if (scanEntriesResult.isErr()) { + this.logger.error("Error while scanning APL entities from DynamoDB", { + error: scanEntriesResult.error, + }); + + return err(scanEntriesResult.error); + } + + return ok( + scanEntriesResult.value.Items?.map(this.segmentAPLMapper.dynamoDBEntityToAuthData) ?? [], + ); + } +} diff --git a/apps/segment/src/modules/db/segment-main-table.ts b/apps/segment/src/modules/db/segment-main-table.ts new file mode 100644 index 000000000..5373992b3 --- /dev/null +++ b/apps/segment/src/modules/db/segment-main-table.ts @@ -0,0 +1,90 @@ +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import { Entity, schema, string, Table } from "dynamodb-toolbox"; + +import { createDynamoDBClient, createDynamoDBDocumentClient } from "@/lib/dynamodb-client"; + +export class SegmentMainTable extends Table< + { name: "PK"; type: "string" }, + { + name: "SK"; + type: "string"; + } +> { + private constructor( + args: ConstructorParameters< + typeof Table< + { name: "PK"; type: "string" }, + { + name: "SK"; + type: "string"; + } + > + >[number], + ) { + super(args); + } + + static create({ + documentClient, + tableName, + }: { + documentClient: DynamoDBDocumentClient; + tableName: string; + }): SegmentMainTable { + return new SegmentMainTable({ + documentClient, + name: tableName, + partitionKey: { name: "PK", type: "string" }, + sortKey: { + name: "SK", + type: "string", + }, + }); + } + + static getAPLPrimaryKey({ saleorApiUrl }: { saleorApiUrl: string }) { + return `${saleorApiUrl}` as const; + } + + static getAPLSortKey() { + return `APL` as const; + } +} + +const SegmentConfigTableSchema = { + apl: schema({ + PK: string().key(), + SK: string().key(), + domain: string().optional(), + token: string(), + saleorApiUrl: string(), + appId: string(), + jwks: string().optional(), + }), +}; + +export const client = createDynamoDBClient(); + +export const documentClient = createDynamoDBDocumentClient(client); + +export const SegmentMainTableEntityFactory = { + createAPLEntity: (table: SegmentMainTable) => { + return new Entity({ + table, + name: "APL", + schema: SegmentConfigTableSchema.apl, + timestamps: { + created: { + name: "createdAt", + savedAs: "createdAt", + }, + modified: { + name: "modifiedAt", + savedAs: "modifiedAt", + }, + }, + }); + }, +}; + +export type SegmentAPLEntityType = ReturnType; diff --git a/apps/segment/src/saleor-app.ts b/apps/segment/src/saleor-app.ts index 30c5c5e0d..eb623057f 100644 --- a/apps/segment/src/saleor-app.ts +++ b/apps/segment/src/saleor-app.ts @@ -3,12 +3,41 @@ import { SaleorApp } from "@saleor/app-sdk/saleor-app"; import { env } from "./env"; import { BaseError } from "./errors"; +import { DynamoAPL } from "./lib/dynamodb-apl"; +import { + documentClient, + SegmentMainTable, + SegmentMainTableEntityFactory, +} from "./modules/db/segment-main-table"; export let apl: APL; const MisconfiguredSaleorCloudAPLError = BaseError.subclass("MisconfiguredSaleorCloudAPLError"); +const MisconfiguredDynamoAPLError = BaseError.subclass("MisconfiguredDynamoAPLError"); switch (env.APL) { + case "dynamodb": { + if ( + !env.DYNAMODB_MAIN_TABLE_NAME || + !env.AWS_REGION || + !env.AWS_ACCESS_KEY_ID || + !env.AWS_SECRET_ACCESS_KEY + ) { + throw new MisconfiguredDynamoAPLError( + "DynamoDB APL is not configured - missing env variables. Check saleor-app.ts", + ); + } + + // TODO: when we have config in DyanamoDB - move to `segment-main-table.ts` + const table = SegmentMainTable.create({ + tableName: env.DYNAMODB_MAIN_TABLE_NAME, + documentClient, + }); + const segmentAPLEntity = SegmentMainTableEntityFactory.createAPLEntity(table); + + apl = new DynamoAPL({ segmentAPLEntity }); + break; + } case "saleor-cloud": { if (!env.REST_APL_ENDPOINT || !env.REST_APL_TOKEN) { throw new MisconfiguredSaleorCloudAPLError( diff --git a/apps/segment/turbo.json b/apps/segment/turbo.json index 11410c5ad..e2206676d 100644 --- a/apps/segment/turbo.json +++ b/apps/segment/turbo.json @@ -17,7 +17,11 @@ "MANIFEST_APP_ID", "SENTRY_ORG", "SENTRY_PROJECT", - "SENTRY_AUTH_TOKEN" + "SENTRY_AUTH_TOKEN", + "DYNAMODB_MAIN_TABLE_NAME", + "AWS_REGION", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY" ] } } From 9106a77ea7a32c27ad07f6aa4b607f61231bfdb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:06:24 +0100 Subject: [PATCH 2/8] Update lock --- pnpm-lock.yaml | 65 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d0495c42..dc7f76479 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,7 +210,7 @@ importers: version: 10.43.1(@trpc/server@10.43.1) '@trpc/next': specifier: 10.43.1 - version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/server': specifier: 10.43.1 version: 10.43.1 @@ -255,7 +255,7 @@ importers: version: 6.2.1 next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -481,7 +481,7 @@ importers: version: 20.0.3 next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) p-ratelimit: specifier: 1.0.1 version: 1.0.1 @@ -656,7 +656,7 @@ importers: version: 2.12.6(graphql@16.7.1) next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) node-fetch: specifier: ^3.2.6 version: 3.2.6 @@ -825,7 +825,7 @@ importers: version: 10.43.1(@trpc/server@10.43.1) '@trpc/next': specifier: 10.43.1 - version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/react-query': specifier: 10.43.1 version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -861,7 +861,7 @@ importers: version: 20.0.3 next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1057,7 +1057,7 @@ importers: version: 2.12.6(graphql@16.7.1) next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1140,6 +1140,15 @@ importers: apps/segment: dependencies: + '@aws-sdk/client-dynamodb': + specifier: 3.651.1 + version: 3.651.1 + '@aws-sdk/lib-dynamodb': + specifier: 3.651.1 + version: 3.651.1(@aws-sdk/client-dynamodb@3.651.1) + '@aws-sdk/util-dynamodb': + specifier: 3.651.1 + version: 3.651.1(@aws-sdk/client-dynamodb@3.651.1) '@hookform/resolvers': specifier: ^3.3.1 version: 3.3.1(react-hook-form@7.44.3(react@18.2.0)) @@ -1242,6 +1251,9 @@ importers: dotenv: specifier: 16.3.1 version: 16.3.1 + dynamodb-toolbox: + specifier: 1.8.2 + version: 1.8.2(@aws-sdk/client-dynamodb@3.651.1)(@aws-sdk/lib-dynamodb@3.651.1(@aws-sdk/client-dynamodb@3.651.1)) escape-string-regexp: specifier: 5.0.0 version: 5.0.0 @@ -1262,7 +1274,7 @@ importers: version: 6.2.1 next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1500,7 +1512,7 @@ importers: version: 6.2.1 next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) nodemailer: specifier: ^6.9.1 version: 6.9.1 @@ -1651,7 +1663,7 @@ importers: version: 12.1.1(eslint@node_modules+eslint) next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) typescript: specifier: 5.5.4 version: 5.5.4 @@ -1859,7 +1871,7 @@ importers: version: link:../eslint-config-saleor next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1920,7 +1932,7 @@ importers: version: link:../eslint-config-saleor next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) typescript: specifier: 5.5.4 version: 5.5.4 @@ -1951,7 +1963,7 @@ importers: version: link:../eslint-config-saleor next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1984,7 +1996,7 @@ importers: version: 6.1.0(modern-errors@7.0.1) next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) semver: specifier: 7.5.1 version: 7.5.1 @@ -13656,13 +13668,13 @@ snapshots: '@aws-crypto/crc32@3.0.0': dependencies: '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.329.0 + '@aws-sdk/types': 3.649.0 tslib: 1.14.1 '@aws-crypto/crc32c@3.0.0': dependencies: '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.329.0 + '@aws-sdk/types': 3.649.0 tslib: 1.14.1 '@aws-crypto/ie11-detection@3.0.0': @@ -13722,7 +13734,7 @@ snapshots: '@aws-crypto/util@3.0.0': dependencies: - '@aws-sdk/types': 3.329.0 + '@aws-sdk/types': 3.649.0 '@aws-sdk/util-utf8-browser': 3.259.0 tslib: 1.14.1 @@ -20687,13 +20699,24 @@ snapshots: react-dom: 18.2.0(react@18.2.0) react-ssr-prepass: 1.5.0(react@18.2.0) - '@trpc/next@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@trpc/next@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@tanstack/react-query': 4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/client': 10.43.1(@trpc/server@10.43.1) '@trpc/react-query': 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/server': 10.43.1 - next: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + next: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-ssr-prepass: 1.5.0(react@18.2.0) + + '@trpc/next@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@tanstack/react-query': 4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@trpc/client': 10.43.1(@trpc/server@10.43.1) + '@trpc/react-query': 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@trpc/server': 10.43.1 + next: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-ssr-prepass: 1.5.0(react@18.2.0) @@ -26177,7 +26200,7 @@ snapshots: neverthrow@6.2.1: {} - next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@next/env': 14.2.3 '@swc/helpers': 0.5.5 @@ -26198,7 +26221,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.3 '@next/swc-win32-ia32-msvc': 14.2.3 '@next/swc-win32-x64-msvc': 14.2.3 - '@opentelemetry/api': 1.9.0 + '@opentelemetry/api': link:node_modules/@opentelemetry/api transitivePeerDependencies: - '@babel/core' - babel-plugin-macros From e2282ddaf1da92f03bcfb471b48f7b08762ed9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:31:11 +0100 Subject: [PATCH 3/8] Fixes after CR --- apps/segment/src/lib/dynamodb-apl.ts | 25 ++++------- .../db/segment-apl-repository-factory.ts | 42 +++++++++++++++++++ .../src/modules/db/segment-apl-repository.ts | 5 ++- .../src/modules/db/segment-main-table.ts | 26 ++++-------- apps/segment/src/modules/db/types.ts | 15 +++++++ apps/segment/src/saleor-app.ts | 27 ++---------- 6 files changed, 80 insertions(+), 60 deletions(-) create mode 100644 apps/segment/src/modules/db/segment-apl-repository-factory.ts create mode 100644 apps/segment/src/modules/db/types.ts diff --git a/apps/segment/src/lib/dynamodb-apl.ts b/apps/segment/src/lib/dynamodb-apl.ts index 14f2a36e9..a0b97446b 100644 --- a/apps/segment/src/lib/dynamodb-apl.ts +++ b/apps/segment/src/lib/dynamodb-apl.ts @@ -1,27 +1,22 @@ import { APL, AplConfiguredResult, AplReadyResult, AuthData } from "@saleor/app-sdk/APL"; +import { env } from "@/env"; import { BaseError } from "@/errors"; -import { SegmentAPLRepository } from "@/modules/db/segment-apl-repository"; -import { SegmentAPLEntityType } from "@/modules/db/segment-main-table"; +import { APLRepository } from "@/modules/db/types"; export class DynamoAPL implements APL { - private segmentAPLRepository: SegmentAPLRepository; - static SetAuthDataError = BaseError.subclass("SetAuthDataError"); static DeleteAuthDataError = BaseError.subclass("DeleteAuthDataError"); static MissingEnvVariablesError = BaseError.subclass("MissingEnvVariablesError"); - constructor({ segmentAPLEntity }: { segmentAPLEntity: SegmentAPLEntityType }) { - this.segmentAPLRepository = new SegmentAPLRepository({ segmentAPLEntity }); - } + constructor(private deps: { repository: APLRepository }) {} async get(saleorApiUrl: string): Promise { - const getEntryResult = await this.segmentAPLRepository.getEntry({ + const getEntryResult = await this.deps.repository.getEntry({ saleorApiUrl, }); if (getEntryResult.isErr()) { - // TODO: should we throw here? return undefined; } @@ -29,7 +24,7 @@ export class DynamoAPL implements APL { } async set(authData: AuthData): Promise { - const setEntryResult = await this.segmentAPLRepository.setEntry({ + const setEntryResult = await this.deps.repository.setEntry({ authData, }); @@ -43,7 +38,7 @@ export class DynamoAPL implements APL { } async delete(saleorApiUrl: string): Promise { - const deleteEntryResult = await this.segmentAPLRepository.deleteEntry({ + const deleteEntryResult = await this.deps.repository.deleteEntry({ saleorApiUrl, }); @@ -57,10 +52,9 @@ export class DynamoAPL implements APL { } async getAll(): Promise { - const getAllEntriesResult = await this.segmentAPLRepository.getAllEntries(); + const getAllEntriesResult = await this.deps.repository.getAllEntries(); if (getAllEntriesResult.isErr()) { - // TODO: should we throw here? return []; } @@ -99,9 +93,8 @@ export class DynamoAPL implements APL { "AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", - ]; + ] as const; - // eslint-disable-next-line node/no-process-env - return variables.every((variable) => !!process.env[variable]); + return variables.every((variable) => !!env[variable]); } } diff --git a/apps/segment/src/modules/db/segment-apl-repository-factory.ts b/apps/segment/src/modules/db/segment-apl-repository-factory.ts new file mode 100644 index 000000000..9bb6799fa --- /dev/null +++ b/apps/segment/src/modules/db/segment-apl-repository-factory.ts @@ -0,0 +1,42 @@ +import { env } from "@/env"; +import { BaseError } from "@/errors"; + +import { SegmentAPLRepository } from "./segment-apl-repository"; +import { + documentClient, + SegmentMainTable, + SegmentMainTableEntityFactory, +} from "./segment-main-table"; + +export class SegmentAPLRepositoryFactory { + static RepositoryCreationError = BaseError.subclass("RepositoryCreationError"); + + static create(): SegmentAPLRepository { + if ( + !env.DYNAMODB_MAIN_TABLE_NAME || + !env.AWS_REGION || + !env.AWS_ACCESS_KEY_ID || + !env.AWS_SECRET_ACCESS_KEY + ) { + throw new SegmentAPLRepositoryFactory.RepositoryCreationError( + "DynamoDB APL is not configured - missing env variables.", + ); + } + + try { + // TODO: when we have config in DyanamoDB - move to `segment-main-table.ts` + const table = SegmentMainTable.create({ + tableName: env.DYNAMODB_MAIN_TABLE_NAME, + documentClient, + }); + const segmentAPLEntity = SegmentMainTableEntityFactory.createAPLEntity(table); + + return new SegmentAPLRepository({ segmentAPLEntity }); + } catch (error) { + throw new SegmentAPLRepositoryFactory.RepositoryCreationError( + "Failed to create DynamoDB APL repository", + { cause: error }, + ); + } + } +} diff --git a/apps/segment/src/modules/db/segment-apl-repository.ts b/apps/segment/src/modules/db/segment-apl-repository.ts index 4a245c453..19af007a9 100644 --- a/apps/segment/src/modules/db/segment-apl-repository.ts +++ b/apps/segment/src/modules/db/segment-apl-repository.ts @@ -7,9 +7,10 @@ import { createLogger } from "@/logger"; import { SegmentAPLEntityType, SegmentMainTable } from "@/modules/db/segment-main-table"; import { SegmentAPLMapper } from "./segment-apl-mapper"; +import { APLRepository } from "./types"; -export class SegmentAPLRepository { - private logger = createLogger("DynamoDBAPLRepository"); +export class SegmentAPLRepository implements APLRepository { + private logger = createLogger("SegmentAPLRepository"); private segmentAPLMapper = new SegmentAPLMapper(); diff --git a/apps/segment/src/modules/db/segment-main-table.ts b/apps/segment/src/modules/db/segment-main-table.ts index 5373992b3..924affbf1 100644 --- a/apps/segment/src/modules/db/segment-main-table.ts +++ b/apps/segment/src/modules/db/segment-main-table.ts @@ -3,24 +3,14 @@ import { Entity, schema, string, Table } from "dynamodb-toolbox"; import { createDynamoDBClient, createDynamoDBDocumentClient } from "@/lib/dynamodb-client"; -export class SegmentMainTable extends Table< - { name: "PK"; type: "string" }, - { - name: "SK"; - type: "string"; - } -> { - private constructor( - args: ConstructorParameters< - typeof Table< - { name: "PK"; type: "string" }, - { - name: "SK"; - type: "string"; - } - > - >[number], - ) { +type PartitionKey = { name: "PK"; type: "string" }; +type SortKey = { name: "SK"; type: "string" }; + +/** + * This table is used to store all relevant data for the Segment application meaning: APL, configuration, etc. + */ +export class SegmentMainTable extends Table { + private constructor(args: ConstructorParameters>[number]) { super(args); } diff --git a/apps/segment/src/modules/db/types.ts b/apps/segment/src/modules/db/types.ts new file mode 100644 index 000000000..03755e19b --- /dev/null +++ b/apps/segment/src/modules/db/types.ts @@ -0,0 +1,15 @@ +import { AuthData } from "@saleor/app-sdk/APL"; +import { Result } from "neverthrow"; + +import { BaseError } from "@/errors"; + +export interface APLRepository { + getEntry(args: { + saleorApiUrl: string; + }): Promise>>; + setEntry(args: { authData: AuthData }): Promise>>; + deleteEntry(args: { + saleorApiUrl: string; + }): Promise>>; + getAllEntries(): Promise>>; +} diff --git a/apps/segment/src/saleor-app.ts b/apps/segment/src/saleor-app.ts index eb623057f..9eb930323 100644 --- a/apps/segment/src/saleor-app.ts +++ b/apps/segment/src/saleor-app.ts @@ -4,38 +4,17 @@ import { SaleorApp } from "@saleor/app-sdk/saleor-app"; import { env } from "./env"; import { BaseError } from "./errors"; import { DynamoAPL } from "./lib/dynamodb-apl"; -import { - documentClient, - SegmentMainTable, - SegmentMainTableEntityFactory, -} from "./modules/db/segment-main-table"; +import { SegmentAPLRepositoryFactory } from "./modules/db/segment-apl-repository-factory"; export let apl: APL; const MisconfiguredSaleorCloudAPLError = BaseError.subclass("MisconfiguredSaleorCloudAPLError"); -const MisconfiguredDynamoAPLError = BaseError.subclass("MisconfiguredDynamoAPLError"); switch (env.APL) { case "dynamodb": { - if ( - !env.DYNAMODB_MAIN_TABLE_NAME || - !env.AWS_REGION || - !env.AWS_ACCESS_KEY_ID || - !env.AWS_SECRET_ACCESS_KEY - ) { - throw new MisconfiguredDynamoAPLError( - "DynamoDB APL is not configured - missing env variables. Check saleor-app.ts", - ); - } - - // TODO: when we have config in DyanamoDB - move to `segment-main-table.ts` - const table = SegmentMainTable.create({ - tableName: env.DYNAMODB_MAIN_TABLE_NAME, - documentClient, - }); - const segmentAPLEntity = SegmentMainTableEntityFactory.createAPLEntity(table); + const repository = SegmentAPLRepositoryFactory.create(); - apl = new DynamoAPL({ segmentAPLEntity }); + apl = new DynamoAPL({ repository }); break; } case "saleor-cloud": { From 3dfe0dc20dbbc66f3e41f4dbeceb3747e9e71bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:00:09 +0100 Subject: [PATCH 4/8] add tests --- apps/segment/package.json | 7 +- apps/segment/src/logger.ts | 2 +- .../modules/db/segment-apl-repository.test.ts | 195 ++++++++++++++++++ .../src/modules/db/segment-main-table.test.ts | 59 ++++++ apps/segment/src/setup-tests.ts | 4 + pnpm-lock.yaml | 130 +++++++----- 6 files changed, 341 insertions(+), 56 deletions(-) create mode 100644 apps/segment/src/modules/db/segment-apl-repository.test.ts create mode 100644 apps/segment/src/modules/db/segment-main-table.test.ts diff --git a/apps/segment/package.json b/apps/segment/package.json index d46484791..bf1078ec5 100644 --- a/apps/segment/package.json +++ b/apps/segment/package.json @@ -19,7 +19,6 @@ "@aws-sdk/client-dynamodb": "3.651.1", "@aws-sdk/lib-dynamodb": "3.651.1", "@aws-sdk/util-dynamodb": "3.651.1", - "dynamodb-toolbox": "1.8.2", "@hookform/resolvers": "^3.3.1", "@opentelemetry/api": "../../node_modules/@opentelemetry/api", "@opentelemetry/api-logs": "../../node_modules/@opentelemetry/api-logs", @@ -54,6 +53,7 @@ "@urql/exchange-auth": "2.1.4", "@vitejs/plugin-react": "4.3.1", "dotenv": "16.3.1", + "dynamodb-toolbox": "1.8.2", "escape-string-regexp": "5.0.0", "graphql": "16.7.1", "graphql-tag": "2.12.6", @@ -83,12 +83,13 @@ "@total-typescript/ts-reset": "0.6.1", "@types/react": "18.2.5", "@types/react-dom": "18.2.5", + "@typescript-eslint/eslint-plugin": "7.15.0", + "@typescript-eslint/parser": "7.15.0", + "aws-sdk-client-mock": "4.0.1", "eslint": "../../node_modules/eslint", "eslint-config-saleor": "workspace:*", "eslint-plugin-neverthrow": "^1.1.4", "eslint-plugin-node": "11.1.0", - "@typescript-eslint/eslint-plugin": "7.15.0", - "@typescript-eslint/parser": "7.15.0", "graphql-config": "5.0.3", "jsdom": "^20.0.3", "node-mocks-http": "^1.12.2", diff --git a/apps/segment/src/logger.ts b/apps/segment/src/logger.ts index 0565fa74f..c0b9e3541 100644 --- a/apps/segment/src/logger.ts +++ b/apps/segment/src/logger.ts @@ -8,7 +8,7 @@ rootLogger.settings.maskValuesOfKeys = ["metadata", "username", "password", "api const require = createRequire(import.meta.url); -if (env.NODE_ENV !== "production") { +if (env.NODE_ENV === "development") { attachLoggerConsoleTransport(rootLogger); } diff --git a/apps/segment/src/modules/db/segment-apl-repository.test.ts b/apps/segment/src/modules/db/segment-apl-repository.test.ts new file mode 100644 index 000000000..d6cadf4dc --- /dev/null +++ b/apps/segment/src/modules/db/segment-apl-repository.test.ts @@ -0,0 +1,195 @@ +import { + DeleteCommand, + DynamoDBDocumentClient, + GetCommand, + PutCommand, + ScanCommand, +} from "@aws-sdk/lib-dynamodb"; +import { AuthData } from "@saleor/app-sdk/APL"; +import { mockClient } from "aws-sdk-client-mock"; +import { SavedItem } from "dynamodb-toolbox"; +import { beforeEach, describe, expect, it } from "vitest"; + +import { SegmentAPLRepository } from "./segment-apl-repository"; +import { SegmentMainTable, SegmentMainTableEntityFactory } from "./segment-main-table"; + +describe("SegmentAPLRepository", () => { + const mockDocumentClient = mockClient(DynamoDBDocumentClient); + + const segmentMainTable = SegmentMainTable.create({ + // @ts-expect-error https://github.com/m-radzikowski/aws-sdk-client-mock/issues/197 + documentClient: mockDocumentClient, + tableName: "segment-test-table", + }); + + const segmentAPLEntity = SegmentMainTableEntityFactory.createAPLEntity(segmentMainTable); + + const mockedAuthData: AuthData = { + appId: "appId", + saleorApiUrl: "saleorApiUrl", + token: "appToken", + }; + + beforeEach(() => { + mockDocumentClient.reset(); + }); + + it("should successfully get AuthData entry from DynamoDB", async () => { + const mockedAPLEntry: SavedItem = { + PK: "saleorApiUrl", + SK: "APL", + token: "appToken", + saleorApiUrl: "saleorApiUrl", + appId: "appId", + _et: "APL", + createdAt: "2023-01-01T00:00:00.000Z", + modifiedAt: "2023-01-01T00:00:00.000Z", + }; + + mockDocumentClient.on(GetCommand, {}).resolvesOnce({ + Item: mockedAPLEntry, + }); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.getEntry({ saleorApiUrl: "saleorApiUrl" }); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toStrictEqual({ + appId: "appId", + domain: undefined, + jwks: undefined, + saleorApiUrl: "saleorApiUrl", + token: "appToken", + }); + }); + + it("should fail to get AuthData entry from DynamoDB if it does not exist", async () => { + mockDocumentClient.on(GetCommand, {}).resolvesOnce({}); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.getEntry({ saleorApiUrl: "saleorApiUrl" }); + + expect(result.isErr()).toBe(true); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.ReadEntityError); + }); + + it("should successfully set AuthData entry in DynamoDB", async () => { + mockDocumentClient.on(PutCommand, {}).resolvesOnce({}); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.setEntry({ + authData: mockedAuthData, + }); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toBe(undefined); + }); + + it("should handle errors when setting AuthData entry DynamoDB", async () => { + mockDocumentClient.on(PutCommand, {}).rejectsOnce("Exception"); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.setEntry({ + authData: mockedAuthData, + }); + + expect(result.isErr()).toBe(true); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.WriteEntityError); + }); + + it("should successfully delete AuthData entry from DynamoDB", async () => { + mockDocumentClient.on(DeleteCommand, {}).resolvesOnce({}); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.deleteEntry({ saleorApiUrl: "saleorApiUrl" }); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toBe(undefined); + }); + + it("should handle errors when deleting AuthData entry from DynamoDB", async () => { + mockDocumentClient.on(DeleteCommand, {}).rejectsOnce("Exception"); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.deleteEntry({ saleorApiUrl: "saleorApiUrl" }); + + expect(result.isErr()).toBe(true); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.DeleteEntityError); + }); + + it("should successfully get all AuthData entries from DynamoDB", async () => { + const mockedAPLEntries: SavedItem[] = [ + { + PK: "saleorApiUrl", + SK: "APL", + token: "appToken", + saleorApiUrl: "saleorApiUrl", + appId: "appId", + _et: "APL", + createdAt: "2023-01-01T00:00:00.000Z", + modifiedAt: "2023-01-01T00:00:00.000Z", + }, + { + PK: "additionalSaleorApiUrl", + SK: "APL", + token: "newAppToken", + saleorApiUrl: "additionalSaleorApiUrl", + appId: "newAppId", + _et: "APL", + createdAt: "2024-01-01T00:00:00.000Z", + modifiedAt: "2024-01-01T00:00:00.000Z", + }, + ]; + + mockDocumentClient.on(ScanCommand, {}).resolvesOnce({ + Items: mockedAPLEntries, + }); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.getAllEntries(); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toStrictEqual([ + { + appId: "appId", + domain: undefined, + jwks: undefined, + saleorApiUrl: "saleorApiUrl", + token: "appToken", + }, + { + appId: "newAppId", + domain: undefined, + jwks: undefined, + saleorApiUrl: "additionalSaleorApiUrl", + token: "newAppToken", + }, + ]); + }); + + it("should handle error when getting all AuthData entries from DynamoDB", async () => { + mockDocumentClient.on(ScanCommand, {}).rejectsOnce("Exception"); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.getAllEntries(); + + expect(result.isErr()).toBe(true); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.ScanEntityError); + }); +}); diff --git a/apps/segment/src/modules/db/segment-main-table.test.ts b/apps/segment/src/modules/db/segment-main-table.test.ts new file mode 100644 index 000000000..f852dfff8 --- /dev/null +++ b/apps/segment/src/modules/db/segment-main-table.test.ts @@ -0,0 +1,59 @@ +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import { mockClient } from "aws-sdk-client-mock"; +import { EntityParser } from "dynamodb-toolbox"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +import { SegmentMainTable, SegmentMainTableEntityFactory } from "./segment-main-table"; + +describe("SegmentMainTable", () => { + const mockDate = new Date("2023-01-01T00:00:00Z"); + + beforeAll(() => { + vi.setSystemTime(mockDate); + }); + + afterEach(() => { + vi.resetModules(); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + const mockDocumentClient = mockClient(DynamoDBDocumentClient); + + const segmentMainTable = SegmentMainTable.create({ + // @ts-expect-error https://github.com/m-radzikowski/aws-sdk-client-mock/issues/197 + documentClient: mockDocumentClient, + tableName: "segment-test-table", + }); + + beforeEach(() => { + mockDocumentClient.reset(); + }); + + describe("SegmentAPLEntity", () => { + it("should create a new entity in DynamoDB with default fields", () => { + const aplEntity = SegmentMainTableEntityFactory.createAPLEntity(segmentMainTable); + + const parseResult = aplEntity.build(EntityParser).parse({ + PK: "saleorApiUrl", + SK: "APL", + token: "appToken", + saleorApiUrl: "saleorApiUrl", + appId: "appId", + }); + + expect(parseResult.item).toStrictEqual({ + PK: "saleorApiUrl", + SK: "APL", + _et: "APL", + appId: "appId", + saleorApiUrl: "saleorApiUrl", + token: "appToken", + createdAt: "2023-01-01T00:00:00.000Z", + modifiedAt: "2023-01-01T00:00:00.000Z", + }); + }); + }); +}); diff --git a/apps/segment/src/setup-tests.ts b/apps/segment/src/setup-tests.ts index cb0ff5c3b..d788c3b29 100644 --- a/apps/segment/src/setup-tests.ts +++ b/apps/segment/src/setup-tests.ts @@ -1 +1,5 @@ +import { vi } from "vitest"; + +vi.stubEnv("SECRET_KEY", "test_secret_key"); + export {}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc7f76479..4a7d9b968 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,7 +210,7 @@ importers: version: 10.43.1(@trpc/server@10.43.1) '@trpc/next': specifier: 10.43.1 - version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/server': specifier: 10.43.1 version: 10.43.1 @@ -255,7 +255,7 @@ importers: version: 6.2.1 next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -481,7 +481,7 @@ importers: version: 20.0.3 next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) p-ratelimit: specifier: 1.0.1 version: 1.0.1 @@ -656,7 +656,7 @@ importers: version: 2.12.6(graphql@16.7.1) next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) node-fetch: specifier: ^3.2.6 version: 3.2.6 @@ -747,7 +747,7 @@ importers: version: 3.3.1(react-hook-form@7.44.3(react@18.2.0)) '@opentelemetry/api': specifier: ../../node_modules/@opentelemetry/api - version: 1.9.0 + version: link:../../node_modules/@opentelemetry/api '@opentelemetry/api-logs': specifier: ../../node_modules/@opentelemetry/api-logs version: link:../../node_modules/@opentelemetry/api-logs @@ -825,7 +825,7 @@ importers: version: 10.43.1(@trpc/server@10.43.1) '@trpc/next': specifier: 10.43.1 - version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/react-query': specifier: 10.43.1 version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -861,7 +861,7 @@ importers: version: 20.0.3 next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1057,7 +1057,7 @@ importers: version: 2.12.6(graphql@16.7.1) next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1154,7 +1154,7 @@ importers: version: 3.3.1(react-hook-form@7.44.3(react@18.2.0)) '@opentelemetry/api': specifier: ../../node_modules/@opentelemetry/api - version: link:../../node_modules/@opentelemetry/api + version: 1.9.0 '@opentelemetry/api-logs': specifier: ../../node_modules/@opentelemetry/api-logs version: link:../../node_modules/@opentelemetry/api-logs @@ -1238,7 +1238,7 @@ importers: version: 10.43.1(@trpc/server@10.43.1) '@trpc/next': specifier: 10.43.1 - version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/server': specifier: 10.43.1 version: 10.43.1 @@ -1274,7 +1274,7 @@ importers: version: 6.2.1 next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1338,22 +1338,25 @@ importers: version: 18.2.5 '@typescript-eslint/eslint-plugin': specifier: 7.15.0 - version: 7.15.0(@typescript-eslint/parser@7.15.0(eslint@node_modules+eslint)(typescript@5.5.4))(eslint@node_modules+eslint)(typescript@5.5.4) + version: 7.15.0(@typescript-eslint/parser@7.15.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/parser': specifier: 7.15.0 - version: 7.15.0(eslint@node_modules+eslint)(typescript@5.5.4) + version: 7.15.0(eslint@8.57.0)(typescript@5.5.4) + aws-sdk-client-mock: + specifier: 4.0.1 + version: 4.0.1 eslint: specifier: ../../node_modules/eslint - version: link:../../node_modules/eslint + version: 8.57.0 eslint-config-saleor: specifier: workspace:* version: link:../../packages/eslint-config-saleor eslint-plugin-neverthrow: specifier: ^1.1.4 - version: 1.1.4(@typescript-eslint/parser@7.15.0(eslint@node_modules+eslint)(typescript@5.5.4))(eslint@node_modules+eslint)(typescript@5.5.4) + version: 1.1.4(@typescript-eslint/parser@7.15.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) eslint-plugin-node: specifier: 11.1.0 - version: 11.1.0(eslint@node_modules+eslint) + version: 11.1.0(eslint@8.57.0) graphql-config: specifier: 5.0.3 version: 5.0.3(@types/node@20.12.3)(graphql@16.7.1)(typescript@5.5.4) @@ -1512,7 +1515,7 @@ importers: version: 6.2.1 next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) nodemailer: specifier: ^6.9.1 version: 6.9.1 @@ -1663,7 +1666,7 @@ importers: version: 12.1.1(eslint@node_modules+eslint) next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) typescript: specifier: 5.5.4 version: 5.5.4 @@ -1871,7 +1874,7 @@ importers: version: link:../eslint-config-saleor next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1932,7 +1935,7 @@ importers: version: link:../eslint-config-saleor next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) typescript: specifier: 5.5.4 version: 5.5.4 @@ -1963,7 +1966,7 @@ importers: version: link:../eslint-config-saleor next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1996,7 +1999,7 @@ importers: version: 6.1.0(modern-errors@7.0.1) next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) semver: specifier: 7.5.1 version: 7.5.1 @@ -20699,24 +20702,13 @@ snapshots: react-dom: 18.2.0(react@18.2.0) react-ssr-prepass: 1.5.0(react@18.2.0) - '@trpc/next@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': - dependencies: - '@tanstack/react-query': 4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@trpc/client': 10.43.1(@trpc/server@10.43.1) - '@trpc/react-query': 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@trpc/server': 10.43.1 - next: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-ssr-prepass: 1.5.0(react@18.2.0) - - '@trpc/next@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@trpc/next@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@tanstack/react-query': 4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/client': 10.43.1(@trpc/server@10.43.1) '@trpc/react-query': 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/server': 10.43.1 - next: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + next: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-ssr-prepass: 1.5.0(react@18.2.0) @@ -21005,15 +20997,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.0 - '@typescript-eslint/eslint-plugin@7.15.0(@typescript-eslint/parser@7.15.0(eslint@node_modules+eslint)(typescript@5.5.4))(eslint@node_modules+eslint)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@7.15.0(@typescript-eslint/parser@7.15.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 7.15.0(eslint@node_modules+eslint)(typescript@5.5.4) + '@typescript-eslint/parser': 7.15.0(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/scope-manager': 7.15.0 - '@typescript-eslint/type-utils': 7.15.0(eslint@node_modules+eslint)(typescript@5.5.4) - '@typescript-eslint/utils': 7.15.0(eslint@node_modules+eslint)(typescript@5.5.4) + '@typescript-eslint/type-utils': 7.15.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/utils': 7.15.0(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/visitor-keys': 7.15.0 - eslint: link:node_modules/eslint + eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -21035,6 +21027,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@7.15.0(eslint@8.57.0)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/scope-manager': 7.15.0 + '@typescript-eslint/types': 7.15.0 + '@typescript-eslint/typescript-estree': 7.15.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 7.15.0 + debug: 4.3.4 + eslint: 8.57.0 + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@7.15.0(eslint@node_modules+eslint)(typescript@5.5.4)': dependencies: '@typescript-eslint/scope-manager': 7.15.0 @@ -21081,12 +21086,12 @@ snapshots: '@typescript-eslint/types': 8.11.0 '@typescript-eslint/visitor-keys': 8.11.0 - '@typescript-eslint/type-utils@7.15.0(eslint@node_modules+eslint)(typescript@5.5.4)': + '@typescript-eslint/type-utils@7.15.0(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@typescript-eslint/typescript-estree': 7.15.0(typescript@5.5.4) - '@typescript-eslint/utils': 7.15.0(eslint@node_modules+eslint)(typescript@5.5.4) + '@typescript-eslint/utils': 7.15.0(eslint@8.57.0)(typescript@5.5.4) debug: 4.3.4 - eslint: link:node_modules/eslint + eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: typescript: 5.5.4 @@ -21174,13 +21179,13 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@7.15.0(eslint@node_modules+eslint)(typescript@5.5.4)': + '@typescript-eslint/utils@7.15.0(eslint@8.57.0)(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@node_modules+eslint) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@typescript-eslint/scope-manager': 7.15.0 '@typescript-eslint/types': 7.15.0 '@typescript-eslint/typescript-estree': 7.15.0(typescript@5.5.4) - eslint: link:node_modules/eslint + eslint: 8.57.0 transitivePeerDependencies: - supports-color - typescript @@ -23561,6 +23566,12 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-es@3.0.1(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + eslint-plugin-es@3.0.1(eslint@node_modules+eslint): dependencies: eslint: link:node_modules/eslint @@ -23641,12 +23652,12 @@ snapshots: object.fromentries: 2.0.8 semver: 6.3.1 - eslint-plugin-neverthrow@1.1.4(@typescript-eslint/parser@7.15.0(eslint@node_modules+eslint)(typescript@5.5.4))(eslint@node_modules+eslint)(typescript@5.5.4): + eslint-plugin-neverthrow@1.1.4(@typescript-eslint/parser@7.15.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4): dependencies: '@types/eslint-utils': 3.0.5 - '@typescript-eslint/parser': 7.15.0(eslint@node_modules+eslint)(typescript@5.5.4) - eslint: link:node_modules/eslint - eslint-utils: 3.0.0(eslint@node_modules+eslint) + '@typescript-eslint/parser': 7.15.0(eslint@8.57.0)(typescript@5.5.4) + eslint: 8.57.0 + eslint-utils: 3.0.0(eslint@8.57.0) tsutils: 3.21.0(typescript@5.5.4) transitivePeerDependencies: - typescript @@ -23661,6 +23672,16 @@ snapshots: transitivePeerDependencies: - typescript + eslint-plugin-node@11.1.0(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + eslint-plugin-es: 3.0.1(eslint@8.57.0) + eslint-utils: 2.1.0 + ignore: 5.2.4 + minimatch: 3.1.2 + resolve: 1.22.8 + semver: 6.3.1 + eslint-plugin-node@11.1.0(eslint@node_modules+eslint): dependencies: eslint: link:node_modules/eslint @@ -23719,6 +23740,11 @@ snapshots: dependencies: eslint-visitor-keys: 1.3.0 + eslint-utils@3.0.0(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 2.1.0 + eslint-utils@3.0.0(eslint@node_modules+eslint): dependencies: eslint: link:node_modules/eslint @@ -26200,7 +26226,7 @@ snapshots: neverthrow@6.2.1: {} - next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@next/env': 14.2.3 '@swc/helpers': 0.5.5 @@ -26221,7 +26247,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.3 '@next/swc-win32-ia32-msvc': 14.2.3 '@next/swc-win32-x64-msvc': 14.2.3 - '@opentelemetry/api': link:node_modules/@opentelemetry/api + '@opentelemetry/api': 1.9.0 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros From 2d35b77167fab9a1b624ddead4e4a9e295ce60a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:54:56 +0100 Subject: [PATCH 5/8] Add tests --- .../lib/__tests__/in-memory-apl-repository.ts | 41 ++++ apps/segment/src/lib/dyanmodb-apl.test.ts | 222 ++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 apps/segment/src/lib/__tests__/in-memory-apl-repository.ts create mode 100644 apps/segment/src/lib/dyanmodb-apl.test.ts diff --git a/apps/segment/src/lib/__tests__/in-memory-apl-repository.ts b/apps/segment/src/lib/__tests__/in-memory-apl-repository.ts new file mode 100644 index 000000000..0528e9fe3 --- /dev/null +++ b/apps/segment/src/lib/__tests__/in-memory-apl-repository.ts @@ -0,0 +1,41 @@ +import { AuthData } from "@saleor/app-sdk/APL"; +import { err, ok, Result } from "neverthrow"; + +import { BaseError } from "@/errors"; +import { APLRepository } from "@/modules/db/types"; + +export class InMemoryAPLRepository implements APLRepository { + public entries: Record = {}; + + async getEntry(args: { + saleorApiUrl: string; + }): Promise>> { + if (this.entries[args.saleorApiUrl]) { + return ok(this.entries[args.saleorApiUrl]); + } + + return err(new BaseError("Error geting entry")); + } + + async setEntry(args: { + authData: AuthData; + }): Promise>> { + this.entries[args.authData.saleorApiUrl] = args.authData; + return ok(undefined); + } + + async deleteEntry(args: { + saleorApiUrl: string; + }): Promise>> { + if (this.entries[args.saleorApiUrl]) { + delete this.entries[args.saleorApiUrl]; + return ok(undefined); + } + + return err(new BaseError("Error deleting entry")); + } + + async getAllEntries(): Promise>> { + return ok(Object.values(this.entries)); + } +} diff --git a/apps/segment/src/lib/dyanmodb-apl.test.ts b/apps/segment/src/lib/dyanmodb-apl.test.ts new file mode 100644 index 000000000..73aab2965 --- /dev/null +++ b/apps/segment/src/lib/dyanmodb-apl.test.ts @@ -0,0 +1,222 @@ +import { AuthData } from "@saleor/app-sdk/APL"; +import { err } from "neverthrow"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { BaseError } from "@/errors"; + +import { InMemoryAPLRepository } from "./__tests__/in-memory-apl-repository"; +import { DynamoAPL } from "./dynamodb-apl"; + +describe("DynamoAPL", () => { + const mockedAuthData: AuthData = { + saleorApiUrl: "saleorApiUrl", + token: "appToken", + domain: "saleorDomain", + appId: "saleorAppId", + }; + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should get auth data if it exists", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + repository.setEntry({ + authData: mockedAuthData, + }); + + const result = await apl.get("saleorApiUrl"); + + expect(result).toStrictEqual(mockedAuthData); + }); + + it("should return undefined if auth data does not exist", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.get("saleorApiUrl"); + + expect(result).toBeUndefined(); + }); + + it("should set auth data", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.set(mockedAuthData); + + expect(result).toBeUndefined(); + + const getEntryResult = await repository.getEntry({ + saleorApiUrl: mockedAuthData.saleorApiUrl, + }); + + expect(getEntryResult._unsafeUnwrap()).toStrictEqual(mockedAuthData); + }); + + it("should throw an error if setting auth data fails", async () => { + const repository = new InMemoryAPLRepository(); + + vi.spyOn(repository, "setEntry").mockResolvedValue(err(new BaseError("Error setting data"))); + + const apl = new DynamoAPL({ repository }); + + await expect(apl.set(mockedAuthData)).rejects.toThrowError(DynamoAPL.SetAuthDataError); + }); + + it("should update existing auth data", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + repository.setEntry({ + authData: mockedAuthData, + }); + + apl.set({ + saleorApiUrl: mockedAuthData.saleorApiUrl, + token: "newAppToken", + domain: "newSaleorDomain", + appId: "newSaleorAppId", + }); + + const getEntryResult = await apl.get(mockedAuthData.saleorApiUrl); + + expect(getEntryResult).toStrictEqual({ + saleorApiUrl: mockedAuthData.saleorApiUrl, + domain: "newSaleorDomain", + appId: "newSaleorAppId", + token: "newAppToken", + }); + }); + + it("should delete auth data", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + repository.setEntry({ + authData: mockedAuthData, + }); + + await apl.delete(mockedAuthData.saleorApiUrl); + + const getEntryResult = await apl.get(mockedAuthData.saleorApiUrl); + + expect(getEntryResult).toBeUndefined(); + }); + + it("should throw an error if deleting auth data fails", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + await expect(apl.delete("saleorApiUrl")).rejects.toThrowError(DynamoAPL.DeleteAuthDataError); + }); + + it("should get all auth data", async () => { + const repository = new InMemoryAPLRepository(); + const secondEntry: AuthData = { + saleorApiUrl: "saleorApiUrl2", + token: "appToken2", + domain: "saleorDomain2", + appId: "saleorAppId2", + }; + const apl = new DynamoAPL({ repository }); + + repository.setEntry({ + authData: mockedAuthData, + }); + + repository.setEntry({ + authData: secondEntry, + }); + + const result = await apl.getAll(); + + expect(result).toStrictEqual([mockedAuthData, secondEntry]); + }); + + it("should return empty array if getting all auth data fails", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + vi.spyOn(repository, "getAllEntries").mockResolvedValue( + err(new BaseError("Error getting data")), + ); + + const result = await apl.getAll(); + + expect(result).toStrictEqual([]); + }); + + it("should return ready:true when APL related env variables are set", async () => { + vi.spyOn(await import("@/env"), "env", "get").mockReturnValue({ + DYNAMODB_MAIN_TABLE_NAME: "table_name", + AWS_REGION: "region", + AWS_ACCESS_KEY_ID: "access_key_id", + AWS_SECRET_ACCESS_KEY: "secret_access_key", + APL: "dynamodb", + APP_LOG_LEVEL: "info", + MANIFEST_APP_ID: "", + OTEL_ENABLED: false, + PORT: 0, + SECRET_KEY: "", + NODE_ENV: "test", + ENV: "local", + }); + + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.isReady(); + + expect(result).toStrictEqual({ ready: true }); + }); + + it("should return ready:false when APL related env variables are not set", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.isReady(); + + expect(result).toStrictEqual({ + ready: false, + error: new DynamoAPL.MissingEnvVariablesError("Missing DynamoDB env variables"), + }); + }); + + it("should return configured:true when APL related env variables are set", async () => { + vi.spyOn(await import("@/env"), "env", "get").mockReturnValue({ + DYNAMODB_MAIN_TABLE_NAME: "table_name", + AWS_REGION: "region", + AWS_ACCESS_KEY_ID: "access_key_id", + AWS_SECRET_ACCESS_KEY: "secret_access_key", + APL: "dynamodb", + APP_LOG_LEVEL: "info", + MANIFEST_APP_ID: "", + OTEL_ENABLED: false, + PORT: 0, + SECRET_KEY: "", + NODE_ENV: "test", + ENV: "local", + }); + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.isConfigured(); + + expect(result).toStrictEqual({ configured: true }); + }); + + it("should return configured:false when APL related env variables are not set", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.isConfigured(); + + expect(result).toStrictEqual({ + configured: false, + error: new DynamoAPL.MissingEnvVariablesError("Missing DynamoDB env variables"), + }); + }); +}); From 768243733684bcc830df96b41c4436ddee0b0321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:37:03 +0100 Subject: [PATCH 6/8] Add tracer --- apps/segment/src/lib/dynamodb-apl.ts | 79 +++++++++++++++++----------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/apps/segment/src/lib/dynamodb-apl.ts b/apps/segment/src/lib/dynamodb-apl.ts index a0b97446b..7483e11c3 100644 --- a/apps/segment/src/lib/dynamodb-apl.ts +++ b/apps/segment/src/lib/dynamodb-apl.ts @@ -1,4 +1,5 @@ import { APL, AplConfiguredResult, AplReadyResult, AuthData } from "@saleor/app-sdk/APL"; +import { getOtelTracer } from "@saleor/apps-otel/src/otel-tracer"; import { env } from "@/env"; import { BaseError } from "@/errors"; @@ -9,56 +10,74 @@ export class DynamoAPL implements APL { static DeleteAuthDataError = BaseError.subclass("DeleteAuthDataError"); static MissingEnvVariablesError = BaseError.subclass("MissingEnvVariablesError"); + private tracer = getOtelTracer(); + constructor(private deps: { repository: APLRepository }) {} async get(saleorApiUrl: string): Promise { - const getEntryResult = await this.deps.repository.getEntry({ - saleorApiUrl, - }); + return this.tracer.startActiveSpan("DynamoAPL.get", async (span) => { + const getEntryResult = await this.deps.repository.getEntry({ + saleorApiUrl, + }); - if (getEntryResult.isErr()) { - return undefined; - } + if (getEntryResult.isErr()) { + span.end(); + return undefined; + } - return getEntryResult.value; + span.end(); + return getEntryResult.value; + }); } async set(authData: AuthData): Promise { - const setEntryResult = await this.deps.repository.setEntry({ - authData, - }); - - if (setEntryResult.isErr()) { - throw new DynamoAPL.SetAuthDataError("Failed to set APL entry", { - cause: setEntryResult.error, + return this.tracer.startActiveSpan("DynamoAPL.set", async (span) => { + const setEntryResult = await this.deps.repository.setEntry({ + authData, }); - } - return undefined; - } + if (setEntryResult.isErr()) { + span.end(); + throw new DynamoAPL.SetAuthDataError("Failed to set APL entry", { + cause: setEntryResult.error, + }); + } - async delete(saleorApiUrl: string): Promise { - const deleteEntryResult = await this.deps.repository.deleteEntry({ - saleorApiUrl, + span.end(); + return undefined; }); + } - if (deleteEntryResult.isErr()) { - throw new DynamoAPL.DeleteAuthDataError("Failed to delete APL entry", { - cause: deleteEntryResult.error, + async delete(saleorApiUrl: string): Promise { + return this.tracer.startActiveSpan("DynamoAPL.delete", async (span) => { + const deleteEntryResult = await this.deps.repository.deleteEntry({ + saleorApiUrl, }); - } - return undefined; + if (deleteEntryResult.isErr()) { + span.end(); + throw new DynamoAPL.DeleteAuthDataError("Failed to delete APL entry", { + cause: deleteEntryResult.error, + }); + } + + span.end(); + return undefined; + }); } async getAll(): Promise { - const getAllEntriesResult = await this.deps.repository.getAllEntries(); + return this.tracer.startActiveSpan("DynamoAPL.getAll", async (span) => { + const getAllEntriesResult = await this.deps.repository.getAllEntries(); - if (getAllEntriesResult.isErr()) { - return []; - } + if (getAllEntriesResult.isErr()) { + span.end(); + return []; + } - return getAllEntriesResult.value; + span.end(); + return getAllEntriesResult.value; + }); } async isReady(): Promise { From e67ddcfe5f0db8ba0460ec49edf6c8b79ba78620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:37:48 +0100 Subject: [PATCH 7/8] Add changeset --- .changeset/breezy-buses-greet.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/breezy-buses-greet.md diff --git a/.changeset/breezy-buses-greet.md b/.changeset/breezy-buses-greet.md new file mode 100644 index 000000000..8549331ac --- /dev/null +++ b/.changeset/breezy-buses-greet.md @@ -0,0 +1,5 @@ +--- +"segment": patch +--- + +Added DynamoDB APL. This APL is using DynamoDB as storage. From af4d5f6e30666aee323ebc7be557a86b38b7f9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:03:48 +0100 Subject: [PATCH 8/8] Return null if there is no data in DynamoDB --- .../lib/__tests__/in-memory-apl-repository.ts | 14 ++++++--- apps/segment/src/lib/dyanmodb-apl.test.ts | 17 ++++++++--- apps/segment/src/lib/dynamodb-apl.ts | 16 ++++++++++ .../modules/db/segment-apl-repository.test.ts | 30 +++++++++++++++++-- .../src/modules/db/segment-apl-repository.ts | 12 +++++--- apps/segment/src/modules/db/types.ts | 4 +-- 6 files changed, 77 insertions(+), 16 deletions(-) diff --git a/apps/segment/src/lib/__tests__/in-memory-apl-repository.ts b/apps/segment/src/lib/__tests__/in-memory-apl-repository.ts index 0528e9fe3..a6dff6bd9 100644 --- a/apps/segment/src/lib/__tests__/in-memory-apl-repository.ts +++ b/apps/segment/src/lib/__tests__/in-memory-apl-repository.ts @@ -9,12 +9,12 @@ export class InMemoryAPLRepository implements APLRepository { async getEntry(args: { saleorApiUrl: string; - }): Promise>> { + }): Promise>> { if (this.entries[args.saleorApiUrl]) { return ok(this.entries[args.saleorApiUrl]); } - return err(new BaseError("Error geting entry")); + return ok(null); } async setEntry(args: { @@ -35,7 +35,13 @@ export class InMemoryAPLRepository implements APLRepository { return err(new BaseError("Error deleting entry")); } - async getAllEntries(): Promise>> { - return ok(Object.values(this.entries)); + async getAllEntries(): Promise>> { + const values = Object.values(this.entries); + + if (values.length === 0) { + return ok(null); + } + + return ok(values); } } diff --git a/apps/segment/src/lib/dyanmodb-apl.test.ts b/apps/segment/src/lib/dyanmodb-apl.test.ts index 73aab2965..bd661f228 100644 --- a/apps/segment/src/lib/dyanmodb-apl.test.ts +++ b/apps/segment/src/lib/dyanmodb-apl.test.ts @@ -41,6 +41,17 @@ describe("DynamoAPL", () => { expect(result).toBeUndefined(); }); + it("should throw an error if getting auth data fails", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + vi.spyOn(repository, "getEntry").mockReturnValue( + Promise.resolve(err(new BaseError("Error getting data"))), + ); + + await expect(apl.get("saleorApiUrl")).rejects.toThrowError(DynamoAPL.GetAuthDataError); + }); + it("should set auth data", async () => { const repository = new InMemoryAPLRepository(); const apl = new DynamoAPL({ repository }); @@ -136,7 +147,7 @@ describe("DynamoAPL", () => { expect(result).toStrictEqual([mockedAuthData, secondEntry]); }); - it("should return empty array if getting all auth data fails", async () => { + it("should throw an error if getting all auth data fails", async () => { const repository = new InMemoryAPLRepository(); const apl = new DynamoAPL({ repository }); @@ -144,9 +155,7 @@ describe("DynamoAPL", () => { err(new BaseError("Error getting data")), ); - const result = await apl.getAll(); - - expect(result).toStrictEqual([]); + await expect(apl.getAll()).rejects.toThrowError(DynamoAPL.GetAllAuthDataError); }); it("should return ready:true when APL related env variables are set", async () => { diff --git a/apps/segment/src/lib/dynamodb-apl.ts b/apps/segment/src/lib/dynamodb-apl.ts index 7483e11c3..bc9658e25 100644 --- a/apps/segment/src/lib/dynamodb-apl.ts +++ b/apps/segment/src/lib/dynamodb-apl.ts @@ -6,8 +6,10 @@ import { BaseError } from "@/errors"; import { APLRepository } from "@/modules/db/types"; export class DynamoAPL implements APL { + static GetAuthDataError = BaseError.subclass("GetAuthDataError"); static SetAuthDataError = BaseError.subclass("SetAuthDataError"); static DeleteAuthDataError = BaseError.subclass("DeleteAuthDataError"); + static GetAllAuthDataError = BaseError.subclass("GetAllAuthDataError"); static MissingEnvVariablesError = BaseError.subclass("MissingEnvVariablesError"); private tracer = getOtelTracer(); @@ -21,6 +23,13 @@ export class DynamoAPL implements APL { }); if (getEntryResult.isErr()) { + span.end(); + throw new DynamoAPL.GetAuthDataError("Failed to get APL entry", { + cause: getEntryResult.error, + }); + } + + if (!getEntryResult.value) { span.end(); return undefined; } @@ -71,6 +80,13 @@ export class DynamoAPL implements APL { const getAllEntriesResult = await this.deps.repository.getAllEntries(); if (getAllEntriesResult.isErr()) { + span.end(); + throw new DynamoAPL.GetAllAuthDataError("Failed to get all APL entries", { + cause: getAllEntriesResult.error, + }); + } + + if (!getAllEntriesResult.value) { span.end(); return []; } diff --git a/apps/segment/src/modules/db/segment-apl-repository.test.ts b/apps/segment/src/modules/db/segment-apl-repository.test.ts index d6cadf4dc..335d6f77c 100644 --- a/apps/segment/src/modules/db/segment-apl-repository.test.ts +++ b/apps/segment/src/modules/db/segment-apl-repository.test.ts @@ -65,8 +65,8 @@ describe("SegmentAPLRepository", () => { }); }); - it("should fail to get AuthData entry from DynamoDB if it does not exist", async () => { - mockDocumentClient.on(GetCommand, {}).resolvesOnce({}); + it("should handle errors when getting AuthData from DynamoDB", async () => { + mockDocumentClient.on(GetCommand, {}).rejectsOnce("Exception"); const repository = new SegmentAPLRepository({ segmentAPLEntity }); @@ -77,6 +77,18 @@ describe("SegmentAPLRepository", () => { expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.ReadEntityError); }); + it("should return null if AuthData entry does not exist in DynamoDB", async () => { + mockDocumentClient.on(GetCommand, {}).resolvesOnce({}); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.getEntry({ saleorApiUrl: "saleorApiUrl" }); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toBe(null); + }); + it("should successfully set AuthData entry in DynamoDB", async () => { mockDocumentClient.on(PutCommand, {}).resolvesOnce({}); @@ -181,6 +193,20 @@ describe("SegmentAPLRepository", () => { ]); }); + it("should return null if there are no AuthData entries in DynamoDB", async () => { + mockDocumentClient.on(ScanCommand, {}).resolvesOnce({ + Items: [], + }); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.getAllEntries(); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toBe(null); + }); + it("should handle error when getting all AuthData entries from DynamoDB", async () => { mockDocumentClient.on(ScanCommand, {}).rejectsOnce("Exception"); diff --git a/apps/segment/src/modules/db/segment-apl-repository.ts b/apps/segment/src/modules/db/segment-apl-repository.ts index 19af007a9..def765b11 100644 --- a/apps/segment/src/modules/db/segment-apl-repository.ts +++ b/apps/segment/src/modules/db/segment-apl-repository.ts @@ -51,7 +51,7 @@ export class SegmentAPLRepository implements APLRepository { if (!getEntryResult.value.Item) { this.logger.warn("APL entry not found", { args }); - return err(new SegmentAPLRepository.ReadEntityError("APL entry not found")); + return ok(null); } return ok(this.segmentAPLMapper.dynamoDBEntityToAuthData(getEntryResult.value.Item)); @@ -128,8 +128,12 @@ export class SegmentAPLRepository implements APLRepository { return err(scanEntriesResult.error); } - return ok( - scanEntriesResult.value.Items?.map(this.segmentAPLMapper.dynamoDBEntityToAuthData) ?? [], - ); + const possibleItems = scanEntriesResult.value.Items ?? []; + + if (possibleItems.length > 0) { + return ok(possibleItems.map(this.segmentAPLMapper.dynamoDBEntityToAuthData)); + } + + return ok(null); } } diff --git a/apps/segment/src/modules/db/types.ts b/apps/segment/src/modules/db/types.ts index 03755e19b..302c892c7 100644 --- a/apps/segment/src/modules/db/types.ts +++ b/apps/segment/src/modules/db/types.ts @@ -6,10 +6,10 @@ import { BaseError } from "@/errors"; export interface APLRepository { getEntry(args: { saleorApiUrl: string; - }): Promise>>; + }): Promise>>; setEntry(args: { authData: AuthData }): Promise>>; deleteEntry(args: { saleorApiUrl: string; }): Promise>>; - getAllEntries(): Promise>>; + getAllEntries(): Promise>>; }