Compare commits

..

2 commits

Author SHA1 Message Date
Lukasz Ostrowski
bbc7a7f8f9 restore invoices 2023-09-07 16:59:20 +02:00
Lukasz Ostrowski
7fa1648349 add ts-reset and adjust codebase 2023-09-07 16:49:56 +02:00
118 changed files with 1196 additions and 1007 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-taxes": minor
---
Added logs for AvaTax and TaxJar. Logs are stored in the app metadata. Only the last 100 events are stored. Each provider configuration has its own logs. You can get to them by a new button "Logs" in the provider table.

View file

@ -0,0 +1,15 @@
---
"saleor-app-emails-and-messages": patch
"saleor-app-data-importer": patch
"saleor-app-products-feed": patch
"saleor-app-invoices": patch
"saleor-app-klaviyo": patch
"saleor-app-segment": patch
"saleor-app-cms-v2": patch
"saleor-app-search": patch
"saleor-app-slack": patch
"saleor-app-taxes": patch
"saleor-app-crm": patch
---
Updated Sentry package

View file

@ -0,0 +1,5 @@
---
"saleor-app-klaviyo": patch
---
Fixed error where config couldn't be saved

View file

@ -0,0 +1,5 @@
---
"saleor-app-search": patch
---
Webhook migration scripts has been moved to the shared package.

View file

@ -1,8 +0,0 @@
#changelog
---
"apps": minor
---
### Added
- `apps/emails-and-messages/.env.template`: Described the new environment variable and how it works
- `apps/emails-and-messages/src/saleor-app.ts`: Added case "redis" for switch(AplType), which takes advantage of the [RedisAPL PR](https://github.com/saleor/app-sdk/pull/287) I submitted

View file

@ -0,0 +1,5 @@
---
"saleor-app-invoices": minor
---
Replace text "loading" messages with skeletons

View file

@ -0,0 +1,5 @@
---
"@saleor/webhook-utils": patch
---
Introduced a new shared package with helpers used for webhook management.

View file

@ -0,0 +1,5 @@
---
"saleor-app-invoices": minor
---
Redesigned app layout. Now app uses shared sections as other apps.

View file

@ -0,0 +1,5 @@
---
"saleor-app-klaviyo": minor
---
Improved app layout to match modern style.

View file

@ -6,12 +6,9 @@ on:
jobs:
assign_creator:
if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }}
runs-on: ubuntu-latest
steps:
- name: Assign PR to creator
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
CREATOR: ${{ github.event.pull_request.user.login }}
run: gh pr edit "$PR_URL" --add-assignee "$CREATOR"
uses: thomaseizinger/assign-pr-creator-action@v1.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,31 +0,0 @@
name: Changesets
on:
pull_request:
types:
- opened
- labeled
- edited
- synchronize
branches-ignore:
- 'changeset-release/**'
jobs:
changeset_check:
name: Changeset added to the PR
# Adding 'skip changesets' label to the PR will skip this job
if: ${{ !contains( github.event.pull_request.labels.*.name, 'skip changeset') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
# check out full history
fetch-depth: 0
- uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
- uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Changeset added
run: npx changeset status --since origin/main

View file

@ -17,7 +17,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 18
- uses: JamieMason/syncpack-github-action@c145cec44b3731b3fe8e859679e240d6ae011f0f
- uses: JamieMason/syncpack-github-action@0.2.2
continue-on-error: true
with:
package-manager: "pnpm"

View file

@ -7,4 +7,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: streetsidesoftware/cspell-action@22e32eb3d70acf30e3fc09bd46edc1d30fb2d6db
- uses: streetsidesoftware/cspell-action@v3

View file

@ -25,11 +25,11 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 16
- uses: pnpm/action-setup@d882d12c64e032187b2edb46d3a0d003b7a43598
- uses: pnpm/action-setup@v2
name: Install pnpm
- run: pnpm install
- name: Create Release Pull Request
uses: changesets/action@f13b1baaa620fde937751f5d2c3572b9da32af23
uses: changesets/action@v1
id: changesets
with:
title: 🚀 Release apps

View file

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
- uses: pnpm/action-setup@v2.2.4
- uses: actions/setup-node@v3
with:
node-version: 18

View file

@ -1 +1 @@
* @saleor/delivery-engineering-js
* @saleor/appstore

View file

@ -1,14 +1,5 @@
# saleor-app-cms-v2
## 2.3.3
### Patch Changes
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 2.3.2
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-cms-v2",
"version": "2.3.3",
"version": "2.3.2",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -13,7 +13,7 @@
},
"dependencies": {
"@datocms/cma-client-browser": "2.0.0",
"@hookform/resolvers": "^3.3.1",
"@hookform/resolvers": "^3.1.0",
"@saleor/app-sdk": "0.43.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
@ -48,7 +48,7 @@
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/cli": "4.0.1",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
"@graphql-codegen/typescript": "4.0.1",
@ -57,6 +57,7 @@
"@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@total-typescript/ts-reset": "^0.5.1",
"@types/qs": "^6.9.7",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",

1
apps/cms-v2/reset.d.ts vendored Normal file
View file

@ -0,0 +1 @@
import "@total-typescript/ts-reset";

View file

@ -19,7 +19,7 @@ export class AppConfig {
connections: [],
};
constructor(initialData?: RootConfig.Shape) {
constructor(initialData?: RootConfig.Shape | unknown) {
if (initialData) {
this.rootData = RootConfig.Schema.parse(initialData);
}

View file

@ -2,6 +2,7 @@ import { BuilderIoProviderConfig } from "@/modules/configuration";
import { WebhookProductVariantFragment } from "../../../../generated/graphql";
import { createLogger } from "@saleor/apps-shared";
import { FieldsMapper } from "../fields-mapper";
import { z } from "zod";
// https://www.builder.io/c/docs/write-api
export class BuilderIoClient {
@ -43,7 +44,7 @@ export class BuilderIoClient {
private async updateProductVariantCall(
builderIoEntryId: string,
variant: WebhookProductVariantFragment
variant: WebhookProductVariantFragment,
) {
try {
const response = await fetch(this.endpoint + `/${builderIoEntryId}`, {
@ -71,13 +72,13 @@ export class BuilderIoClient {
{
entriesToUpdate,
},
"Trying to update variants in builder.io with following IDs"
"Trying to update variants in builder.io with following IDs",
);
return Promise.all(
entriesToUpdate.map((id) => {
return this.updateProductVariantCall(id, variant);
})
}),
);
}
@ -94,7 +95,7 @@ export class BuilderIoClient {
return Promise.all(
entriesToUpdate.map((id) => {
return this.updateProductVariantCall(id, variant);
})
}),
);
}
}
@ -112,8 +113,8 @@ export class BuilderIoClient {
"Content-Type": "application/json",
Authorization: `Bearer ${this.config.privateApiKey}`,
},
})
)
}),
),
);
}
@ -127,15 +128,23 @@ export class BuilderIoClient {
variantID: variantId,
variantFieldMapping: this.config.productVariantFieldsMapping.variantId,
},
"Trying to fetch variant from Builder.io"
"Trying to fetch variant from Builder.io",
);
const expectedSchema = z.object({
results: z.array(
z.object({
id: z.string(),
}),
),
});
return fetch(
`https://cdn.builder.io/api/v3/content/${this.config.modelName}?apiKey=${this.config.publicApiKey}&query.data.${this.config.productVariantFieldsMapping.variantId}.$eq=${variantId}&limit=10&includeUnpublished=false&cacheSeconds=0`
`https://cdn.builder.io/api/v3/content/${this.config.modelName}?apiKey=${this.config.publicApiKey}&query.data.${this.config.productVariantFieldsMapping.variantId}.$eq=${variantId}&limit=10&includeUnpublished=false&cacheSeconds=0`,
)
.then((res) => res.json())
.then((res) => expectedSchema.parse(res.json()))
.then((data) => {
return data.results.map((result: any) => result.id) as string[];
return data.results.map((result) => result.id) as string[];
})
.catch((err) => {
this.logger.error(err, "Failed to fetch builder.io entry id");

View file

@ -1,13 +1,5 @@
# saleor-app-crm
## 1.7.8
### Patch Changes
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 1.7.7
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-crm",
"version": "1.7.8",
"version": "1.7.7",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -42,7 +42,7 @@
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/cli": "4.0.1",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
"@graphql-codegen/typescript": "4.0.1",
@ -51,6 +51,7 @@
"@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@total-typescript/ts-reset": "^0.5.1",
"@types/mailchimp__mailchimp_marketing": "^3.0.7",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",

1
apps/crm/reset.d.ts vendored Normal file
View file

@ -0,0 +1 @@
import "@total-typescript/ts-reset";

View file

@ -23,7 +23,7 @@ export const AppBridgePersistence = {
}
try {
return JSON.parse(storageItem as string);
return JSON.parse(storageItem as string) as AppBridgeStorageState;
} catch (e) {
return null;
}

View file

@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { MailchimpConfigSettingsManagerV1 } from "./mailchimp-config-settings-manager";
import { MailchimpConfigSettingsManagerV1, MailchimpConfigType } from "./mailchimp-config-settings-manager";
import { Client } from "urql";
import { SettingsManager, SettingsValue } from "@saleor/app-sdk/settings-manager";
@ -70,9 +70,11 @@ describe("MailchimpConfigSettingsManagerV1", () => {
dc: "us41",
});
const parsedSetValue = JSON.parse(valueHasBeenSet!);
const parsedSetValue = JSON.parse(valueHasBeenSet!) as {
config: MailchimpConfigType
};
expect(parsedSetValue.config.customerCreateEvent.enabled).toBe(false);
expect(parsedSetValue.config.customerCreateEvent?.enabled).toBe(false);
});
it("Calls settings manager with default customerCreateEvent setting to be disabled", async () => {
@ -90,9 +92,11 @@ describe("MailchimpConfigSettingsManagerV1", () => {
dc: "us41",
});
const parsedSetValue = JSON.parse(valueHasBeenSet!);
const parsedSetValue = JSON.parse(valueHasBeenSet!) as {
config: MailchimpConfigType
};
expect(parsedSetValue.config.customerCreateEvent.enabled).toBe(false);
expect(parsedSetValue.config.customerCreateEvent?.enabled).toBe(false);
});
it(".get returns null if data doesnt match schema", async () => {

View file

@ -1,6 +1,8 @@
import { NextApiHandler } from "next";
import { MailchimpClientOAuth } from "../../../../modules/mailchimp/mailchimp-client";
import { createLogger } from "@saleor/apps-shared";
import { z } from "zod";
import * as Sentry from "@sentry/nextjs";
export const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => {
const { host, "x-forwarded-proto": protocol = "http" } = headers;
@ -8,6 +10,17 @@ export const getBaseUrl = (headers: { [name: string]: string | string[] | undefi
return `${protocol}://${host}`;
};
const tokenResponseSchema = z.object({
access_token: z.string().min(1),
});
const metadataResponseSchema = z.object({
dc: z.string().min(1),
login: z.object({
email: z.string().min(1),
}),
});
const handler: NextApiHandler = async (req, res) => {
const baseUrl = getBaseUrl(req.headers);
@ -28,25 +41,46 @@ const handler: NextApiHandler = async (req, res) => {
}),
});
const { access_token } = await tokenResponse.json();
let accessToken: string;
logger.debug({ access_token }, "Received mailchimp access_token");
try {
const tokenResponseJson = await tokenResponse.json();
const parsedTokenResponse = tokenResponseSchema.parse(tokenResponseJson);
accessToken = parsedTokenResponse.access_token;
} catch {
Sentry.captureException(
"Mailchimp token response doesnt contain access_token or can't be fetched",
);
return res.status(500).end();
}
logger.debug({ access_token: accessToken }, "Received mailchimp access_token");
try {
const metadataResponse = await fetch("https://login.mailchimp.com/oauth2/metadata", {
headers: {
Authorization: `OAuth ${access_token}`,
Authorization: `OAuth ${accessToken}`,
},
});
const metadata = await metadataResponse.json();
const metadataJson = await metadataResponse.json();
const mc = new MailchimpClientOAuth(metadata.dc, access_token);
const parsedMetadata = metadataResponseSchema.parse(metadataJson);
const mc = new MailchimpClientOAuth(parsedMetadata.dc, accessToken);
await mc.ping();
return res.redirect(
`/configuration/mailchimp/oauth-success?token=${access_token}&email=${metadata.login.email}&dc=${metadata.dc}`
); // todo maybe move to cookie
`/configuration/mailchimp/oauth-success?token=${accessToken}&email=${parsedMetadata.login.email}&dc=${parsedMetadata.dc}`,
);
} catch {
Sentry.captureException("Mailchimp oauth metadata cant be fetched or is malformed");
return res.status(500).end();
}
};
export default handler;

View file

@ -1,13 +1,5 @@
# saleor-app-data-importer
## 1.9.7
### Patch Changes
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 1.9.6
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-data-importer",
"version": "1.9.7",
"version": "1.9.6",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -39,7 +39,7 @@
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/cli": "4.0.1",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/schema-ast": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
@ -49,6 +49,7 @@
"@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@total-typescript/ts-reset": "^0.5.1",
"@types/dot-object": "^2.1.2",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",

1
apps/data-importer/reset.d.ts vendored Normal file
View file

@ -0,0 +1 @@
import "@total-typescript/ts-reset";

View file

@ -7,12 +7,6 @@ APL=
REST_APL_ENDPOINT=
REST_APL_TOKEN=
# To use Redis as an APL store, set APP_API_BASE_URL and REDIS_URL.
# URL is in format redis[s]://[[username][:password]@][host][:port][/db-number],
# so for example redis://alice:foobared@awesome.redis.server:6380
# For saleor-platform, thats: `redis://redis:6379/1`
REDIS_URL=
APP_LOG_LEVEL=info
# Local development variables. When developped locally with Saleor inside docker, these can be set to:
@ -22,4 +16,3 @@ APP_LOG_LEVEL=info
# https://docs.saleor.io/docs/3.x/developer/extending/apps/local-app-development
APP_IFRAME_BASE_URL=
APP_API_BASE_URL=

View file

@ -1,14 +1,5 @@
# saleor-app-emails-and-messages
## 1.9.10
### Patch Changes
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 1.9.9
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-emails-and-messages",
"version": "1.9.10",
"version": "1.9.9",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -12,7 +12,7 @@
"test": "vitest"
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@hookform/resolvers": "^3.1.0",
"@monaco-editor/react": "^4.4.6",
"@saleor/app-sdk": "0.43.1",
"@saleor/apps-shared": "workspace:*",
@ -51,7 +51,7 @@
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/cli": "4.0.1",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/schema-ast": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
@ -61,6 +61,7 @@
"@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@total-typescript/ts-reset": "^0.5.1",
"@types/html-to-text": "^9.0.0",
"@types/mjml": "^4.7.0",
"@types/nodemailer": "^6.4.7",

1
apps/emails-and-messages/reset.d.ts vendored Normal file
View file

@ -0,0 +1 @@
import "@total-typescript/ts-reset";

View file

@ -1,4 +1,4 @@
import { APL, FileAPL, RedisAPL, SaleorCloudAPL, UpstashAPL } from "@saleor/app-sdk/APL";
import { APL, FileAPL, SaleorCloudAPL, UpstashAPL } from "@saleor/app-sdk/APL";
import { SaleorApp } from "@saleor/app-sdk/saleor-app";
const aplType = process.env.APL ?? "file";
@ -6,12 +6,6 @@ const aplType = process.env.APL ?? "file";
export let apl: APL;
switch (aplType) {
case "redis": {
if (!process.env.REDIS_URL) throw new Error("Missing redis url");
if (!process.env.APP_API_BASE_URL)
throw new Error("Redis relies on APP_API_BASE_URL to store keys, please set env variable");
apl = new RedisAPL(new URL(process.env.REDIS_URL), process.env.APP_API_BASE_URL);
}
case "upstash":
apl = new UpstashAPL();

View file

@ -1,7 +1,5 @@
{
"extends": [
"//"
],
"extends": ["//"],
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
@ -23,8 +21,7 @@
"NEXT_PUBLIC_SENTRY_DSN",
"SENTRY_ENVIRONMENT",
"APP_IFRAME_BASE_URL",
"APP_API_BASE_URL",
"REDIS_URL"
"APP_API_BASE_URL"
]
}
}

View file

@ -1,19 +1,5 @@
# saleor-app-invoices
## 1.16.0
### Minor Changes
- 4aee4e1: Replace text "loading" messages with skeletons
- 4aee4e1: Redesigned app layout. Now app uses shared sections as other apps.
### Patch Changes
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 1.15.7
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-invoices",
"version": "1.16.0",
"version": "1.15.7",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -12,7 +12,7 @@
"test": "vitest"
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@hookform/resolvers": "^3.1.0",
"@saleor/app-sdk": "0.43.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
@ -42,7 +42,7 @@
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/cli": "4.0.1",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/schema-ast": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",

1
apps/invoices/reset.d.ts vendored Normal file
View file

@ -0,0 +1 @@
import "@total-typescript/ts-reset";

View file

@ -37,7 +37,7 @@ const runMigration = async () => {
.catch((e) => {
console.error("❌ Error removing metadata", e);
});
})
}),
);
console.log(results);

View file

@ -44,7 +44,7 @@ const runMigration = async () => {
.catch((e) => {
console.log(
`🚫 failed to create empty config for ${env.saleorApiUrl}. Env may not exist.`,
e.message
e.message,
);
});
}
@ -60,7 +60,7 @@ const runMigration = async () => {
.catch((e) => {
console.error("🚫 Failed to migrate ", env.saleorApiUrl, e);
});
})
}),
);
};

View file

@ -38,7 +38,7 @@ const runReport = async () => {
metadata: metadata,
env: env.saleorApiUrl,
}));
})
}),
);
const report = results.map((r: any) => ({

View file

@ -47,7 +47,7 @@ export const appConfigurationRouter = router({
.input(
z.object({
channelSlug: z.string(),
})
}),
)
.mutation(async ({ ctx, input }) => {
const appConfigV2 =

View file

@ -17,7 +17,10 @@ export interface AppConfigurator {
export class PrivateMetadataAppConfiguratorV1 implements AppConfigurator {
private metadataKey = "app-config";
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
constructor(
private metadataManager: SettingsManager,
private saleorApiUrl: string,
) {}
getConfig(): Promise<AppConfigV1 | undefined> {
return this.metadataManager.get(this.metadataKey, this.saleorApiUrl).then((data) => {

View file

@ -18,13 +18,13 @@ describe("config-v1-to-v2-migration.service", () => {
service = new ConfigV1ToV2MigrationService(mockClient, "https://example.com/graphql/");
vi.spyOn(service.configMetadataManager, "set").mockImplementationOnce(async () =>
Promise.resolve()
Promise.resolve(),
);
});
it("Returns a pure V2 config if V1 config is not present", async () => {
vi.spyOn(service.metadataV1AppConfigurator, "getConfig").mockImplementationOnce(async () =>
Promise.resolve(undefined)
Promise.resolve(undefined),
);
const migrationResult = await service.migrate();
@ -41,7 +41,7 @@ describe("config-v1-to-v2-migration.service", () => {
address: getMockAddress(),
},
},
})
}),
);
const migrationResult = await service.migrate();
@ -49,13 +49,13 @@ describe("config-v1-to-v2-migration.service", () => {
expect(migrationResult.getChannelsOverrides()).toEqual(
expect.objectContaining({
"default-channel": expect.objectContaining(getMockAddress()),
})
}),
);
});
it("Runs a beforeSave callback and saves modified state in metadata - missing v1 config scenario", async () => {
vi.spyOn(service.metadataV1AppConfigurator, "getConfig").mockImplementationOnce(async () =>
Promise.resolve(undefined)
Promise.resolve(undefined),
);
const beforeSaveCb = vi.fn().mockImplementationOnce((config: AppConfigV2) => {
@ -79,7 +79,7 @@ describe("config-v1-to-v2-migration.service", () => {
address: getMockAddress(),
},
},
})
}),
);
const beforeSaveCb = vi.fn().mockImplementationOnce((config: AppConfigV2) => {

View file

@ -10,12 +10,15 @@ export class ConfigV1ToV2MigrationService {
configMetadataManager: AppConfigV2MetadataManager;
metadataV1AppConfigurator: PrivateMetadataAppConfiguratorV1;
constructor(private client: SimpleGraphqlClient, private saleorApiUrl: string) {
constructor(
private client: SimpleGraphqlClient,
private saleorApiUrl: string,
) {
this.settingsManager = createSettingsManager(client);
this.configMetadataManager = new AppConfigV2MetadataManager(this.settingsManager);
this.metadataV1AppConfigurator = new PrivateMetadataAppConfiguratorV1(
this.settingsManager,
this.saleorApiUrl
this.saleorApiUrl,
);
}

View file

@ -22,7 +22,7 @@ describe("ConfigV1ToV2Transformer", function () {
expect(v2.getChannelsOverrides()).toEqual(
expect.objectContaining({
"default-channel": getMockAddress(),
})
}),
);
});
@ -42,7 +42,7 @@ describe("ConfigV1ToV2Transformer", function () {
expect.objectContaining({
"default-channel": getMockAddress(),
"custom-channel": getMockAddress(),
})
}),
);
});
@ -66,7 +66,7 @@ describe("ConfigV1ToV2Transformer", function () {
...getMockAddress(),
city: "",
},
})
}),
);
});
});

View file

@ -8,7 +8,7 @@ export class MicroinvoiceInvoiceGenerator implements InvoiceGenerator {
constructor(
private settings = {
locale: "en-US",
}
},
) {}
async generate(input: {
order: OrderPayloadFragment;
@ -19,17 +19,7 @@ export class MicroinvoiceInvoiceGenerator implements InvoiceGenerator {
const { invoiceNumber, order, companyAddressData, filename } = input;
const microinvoiceInstance = new Microinvoice({
style: {
/*
* header: {
* image: {
* path: "./examples/logo.png",
* width: 50,
* height: 19,
* },
* },
*/
},
style: {},
data: {
invoice: {
name: `Invoice ${invoiceNumber}`,
@ -63,12 +53,6 @@ export class MicroinvoiceInvoiceGenerator implements InvoiceGenerator {
order.billingAddress?.country.country,
],
},
/*
* {
* label: "Tax Identifier",
* value: "todo",
* },
*/
],
seller: [
@ -84,28 +68,9 @@ export class MicroinvoiceInvoiceGenerator implements InvoiceGenerator {
companyAddressData.countryArea,
],
},
/*
* {
* label: "Tax Identifier",
* value: "todo",
* },
*/
],
legal: [
/*
* {
* value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
* weight: "bold",
* color: "primary",
* },
* {
* value: "sed do eiusmod tempor incididunt ut labore et dolore magna.",
* weight: "bold",
* color: "secondary",
* },
*/
],
legal: [],
details: {
header: [

View file

@ -0,0 +1,10 @@
export type ShopAddress = {
city: string;
cityArea: string;
companyName: string;
country: string;
countryArea: string;
postalCode: string;
streetAddress1: string;
streetAddress2: string;
};

View file

@ -156,7 +156,7 @@ const invoiceNumberGenerator = new InvoiceNumberGenerator();
export const handler: NextWebhookApiHandler<InvoiceRequestedPayloadFragment> = async (
req,
res,
context
context,
) => {
const { authData, payload, baseUrl } = context;
const logger = createLogger({ domain: authData.saleorApiUrl, url: baseUrl });
@ -176,7 +176,7 @@ export const handler: NextWebhookApiHandler<InvoiceRequestedPayloadFragment> = a
*/
const invoiceName = invoiceNumberGenerator.generateFromOrder(
order as OrderPayloadFragment,
InvoiceNumberGenerationStrategy.localizedDate("en-US") // todo connect locale -> where from?
InvoiceNumberGenerationStrategy.localizedDate("en-US"), // todo connect locale -> where from?
);
Sentry.addBreadcrumb({
@ -270,7 +270,7 @@ export const handler: NextWebhookApiHandler<InvoiceRequestedPayloadFragment> = a
await new InvoiceCreateNotifier(client).notifyInvoiceCreated(
orderId,
invoiceName,
uploadedFileUrl
uploadedFileUrl,
);
Sentry.addBreadcrumb({

View file

@ -1,18 +1,5 @@
# saleor-app-klaviyo
## 1.9.0
### Minor Changes
- 4aee4e1: Improved app layout to match modern style.
### Patch Changes
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
- 4aee4e1: Fixed error where config couldn't be saved
## 1.8.6
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-klaviyo",
"version": "1.9.0",
"version": "1.8.6",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -35,7 +35,7 @@
"vitest": "0.34.1"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/cli": "4.0.1",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/schema-ast": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
@ -43,6 +43,7 @@
"@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/typescript-urql": "3.7.3",
"@graphql-typed-document-node/core": "3.2.0",
"@total-typescript/ts-reset": "^0.5.1",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",
"autoprefixer": "^10.4.7",

1
apps/klaviyo/reset.d.ts vendored Normal file
View file

@ -0,0 +1 @@
import "@total-typescript/ts-reset";

View file

@ -56,13 +56,13 @@ export const customerCreatedWebhook = new SaleorAsyncWebhook<CustomerCreatedWebh
event: "CUSTOMER_CREATED",
apl: saleorApp.apl,
query: UntypedCustomerCreatedDocument,
}
},
);
const handler: NextWebhookApiHandler<CustomerCreatedWebhookPayloadFragment> = async (
req,
res,
context
context,
) => {
console.debug("customerCreatedWebhook handler called");
@ -94,7 +94,8 @@ const handler: NextWebhookApiHandler<CustomerCreatedWebhookPayloadFragment> = as
const klaviyoResponse = await klaviyoClient.send(klaviyoMetric, userEmail, payload);
if (klaviyoResponse.status !== 200) {
const klaviyoMessage = ` Message: ${(await klaviyoResponse.json())?.message}.` || "";
const klaviyoMessage =
` Message: ${((await klaviyoResponse.json()) as { message: string })?.message}.` || "";
console.debug("Klaviyo returned error: ", klaviyoMessage);

View file

@ -68,7 +68,7 @@ export const fulfillmentCreatedWebhook =
const handler: NextWebhookApiHandler<FulfillmentCreatedWebhookPayloadFragment> = async (
req,
res,
context
context,
) => {
console.debug("fulfillmentCreatedWebhook handler called");
@ -98,7 +98,8 @@ const handler: NextWebhookApiHandler<FulfillmentCreatedWebhookPayloadFragment> =
const klaviyoResponse = await klaviyoClient.send(klaviyoMetric, userEmail, payload);
if (klaviyoResponse.status !== 200) {
const klaviyoMessage = ` Message: ${(await klaviyoResponse.json())?.message}.` || "";
const klaviyoMessage =
` Message: ${((await klaviyoResponse.json()) as { message: string })?.message}.` || "";
console.debug("Klaviyo returned error: ", klaviyoMessage);

View file

@ -38,7 +38,7 @@ export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderCreatedWebhookPay
const handler: NextWebhookApiHandler<OrderCreatedWebhookPayloadFragment> = async (
req,
res,
context
context,
) => {
console.debug("orderCreatedWebhook handler called");
@ -69,7 +69,8 @@ const handler: NextWebhookApiHandler<OrderCreatedWebhookPayloadFragment> = async
const klaviyoResponse = await klaviyoClient.send(klaviyoMetric, userEmail, payload);
if (klaviyoResponse.status !== 200) {
const klaviyoMessage = ` Message: ${(await klaviyoResponse.json())?.message}.` || "";
const klaviyoMessage =
` Message: ${((await klaviyoResponse.json()) as { message: string })?.message}.` || "";
console.debug("Klaviyo returned error: ", klaviyoMessage);
return res.status(500).json({

View file

@ -38,7 +38,7 @@ export const orderFullyPaidWebhook = new SaleorAsyncWebhook<OrderFullyPaidWebhoo
const handler: NextWebhookApiHandler<OrderFullyPaidWebhookPayloadFragment> = async (
req,
res,
context
context,
) => {
console.debug("orderFullyPaidWebhook handler called");
@ -70,7 +70,8 @@ const handler: NextWebhookApiHandler<OrderFullyPaidWebhookPayloadFragment> = asy
const klaviyoResponse = await klaviyoClient.send(klaviyoMetric, userEmail, payload);
if (klaviyoResponse.status !== 200) {
const klaviyoMessage = ` Message: ${(await klaviyoResponse.json())?.message}.` || "";
const klaviyoMessage =
` Message: ${((await klaviyoResponse.json()) as { message: string })?.message}.` || "";
console.debug("Klaviyo returned error: ", klaviyoMessage);

View file

@ -1,21 +1,5 @@
# saleor-app-products-feed
## 1.12.2
### Patch Changes
- ae6dbb1: Removed webhooks on product changes used for feed cache due to changed max execution time.
- ae6dbb1: Changed Vercel's maximum execution time to be 5 minutes for feed generation. This should help with the previous limits of 60s, that was not enough for feed to be generated.
## 1.12.1
### Patch Changes
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 1.12.0
### Minor Changes

View file

@ -0,0 +1,8 @@
fragment ProductVariantWebhookPayload on ProductVariant {
channel
channelListings {
channel {
slug
}
}
}

View file

@ -0,0 +1,8 @@
fragment ProductWebhookPayload on Product {
channel
channelListings {
channel {
slug
}
}
}

View file

@ -0,0 +1,9 @@
subscription ProductCreated {
event {
... on ProductCreated {
product {
...ProductWebhookPayload
}
}
}
}

View file

@ -0,0 +1,9 @@
subscription ProductDeleted {
event {
... on ProductDeleted {
product {
...ProductWebhookPayload
}
}
}
}

View file

@ -0,0 +1,9 @@
subscription ProductUpdated {
event {
... on ProductUpdated {
product {
...ProductWebhookPayload
}
}
}
}

View file

@ -0,0 +1,9 @@
subscription ProductVariantCreated {
event {
... on ProductVariantCreated {
productVariant {
...ProductVariantWebhookPayload
}
}
}
}

View file

@ -0,0 +1,10 @@
subscription ProductVariantDeleted {
event {
... on ProductVariantDeleted {
productVariant {
...ProductVariantWebhookPayload
}
}
}
}

View file

@ -0,0 +1,9 @@
subscription ProductVariantUpdated {
event {
... on ProductVariantUpdated {
productVariant {
...ProductVariantWebhookPayload
}
}
}
}

View file

@ -6,12 +6,7 @@ const isSentryPropertiesInEnvironment =
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
transpilePackages: [
"@saleor/apps-shared",
"@saleor/apps-ui",
"@saleor/react-hook-form-macaw",
"@saleor/webhook-utils",
],
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
};
const configWithSentry = withSentryConfig(
@ -27,7 +22,7 @@ const configWithSentry = withSentryConfig(
tunnelRoute: "/monitoring",
hideSourceMaps: true,
disableLogger: true,
},
}
);
module.exports = isSentryPropertiesInEnvironment ? configWithSentry : nextConfig;

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-products-feed",
"version": "1.12.2",
"version": "1.12.0",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -13,13 +13,12 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.332.0",
"@hookform/resolvers": "^3.3.1",
"@hookform/resolvers": "^3.1.0",
"@saleor/app-sdk": "0.43.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.127",
"@saleor/react-hook-form-macaw": "workspace:*",
"@saleor/webhook-utils": "workspace:*",
"@sentry/nextjs": "7.67.0",
"@tanstack/react-query": "4.29.19",
"@trpc/client": "10.38.1",
@ -28,7 +27,6 @@
"@trpc/server": "10.38.1",
"@urql/exchange-auth": "^2.1.4",
"@vitejs/plugin-react": "4.0.4",
"dotenv": "^16.3.1",
"fast-xml-parser": "^4.0.15",
"graphql": "16.7.1",
"graphql-tag": "^2.12.6",
@ -48,7 +46,7 @@
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/cli": "4.0.1",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/schema-ast": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
@ -58,6 +56,7 @@
"@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@total-typescript/ts-reset": "^0.5.1",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",
"eslint": "8.46.0",

1
apps/products-feed/reset.d.ts vendored Normal file
View file

@ -0,0 +1 @@
import "@total-typescript/ts-reset";

View file

@ -1,7 +0,0 @@
# Webhook migration scripts
Test migration with dry run, operation will not modify any data:
`npx tsx scripts/migrations/run-webhooks-migration-dry-run.ts`
To start the migration run command:
`npx tsx scripts/migrations/run-webhooks-migration.ts`

View file

@ -1,20 +0,0 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { SaleorCloudAPL } from "@saleor/app-sdk/APL";
export const verifyRequiredEnvs = () => {
const requiredEnvs = ["SALEOR_CLOUD_TOKEN", "SALEOR_CLOUD_RESOURCE_URL"];
if (!requiredEnvs.every((env) => process.env[env])) {
throw new Error(`Missing envs: ${requiredEnvs.join(" | ")}`);
}
};
export const fetchCloudAplEnvs = () => {
const saleorAPL = new SaleorCloudAPL({
token: process.env.SALEOR_CLOUD_TOKEN!,
resourceUrl: process.env.SALEOR_CLOUD_RESOURCE_URL!,
});
return saleorAPL.getAll();
};

View file

@ -1,30 +0,0 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils";
import { updateWebhooksScript } from "./update-webhooks";
dotenv.config();
const runMigration = async () => {
console.log("Starting webhooks migration (dry run)");
verifyRequiredEnvs();
console.log("Envs verified, fetching envs");
const allEnvs = await fetchCloudAplEnvs().catch((r) => {
console.error("Could not fetch instances from the APL");
console.error(r);
process.exit(1);
});
for (const env of allEnvs) {
await updateWebhooksScript({ authData: env, dryRun: true });
}
console.log("Migration dry run complete");
};
runMigration();

View file

@ -1,30 +0,0 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils";
import { updateWebhooksScript } from "./update-webhooks";
dotenv.config();
const runMigration = async () => {
console.log("Starting running migration");
verifyRequiredEnvs();
console.log("Envs verified, fetching envs");
const allEnvs = await fetchCloudAplEnvs().catch((r) => {
console.error("Could not fetch instances from the APL");
console.error(r);
process.exit(1);
});
for (const env of allEnvs) {
await updateWebhooksScript({ authData: env, dryRun: false });
}
console.log("Migration complete");
};
runMigration();

View file

@ -1,29 +0,0 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { createGraphQLClient } from "@saleor/apps-shared";
import { AuthData } from "@saleor/app-sdk/APL";
import { webhookMigrationRunner } from "@saleor/webhook-utils";
export const updateWebhooksScript = async ({
authData,
dryRun,
}: {
authData: AuthData;
dryRun: boolean;
}) => {
console.log("Working on env: ", authData.saleorApiUrl);
const client = createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
await webhookMigrationRunner({
client,
dryRun,
getManifests: async ({ appDetails }) => {
// Products feed application has currently no webhooks, so we return empty array
return [];
},
});
};

View file

@ -94,7 +94,6 @@ describe("AppConfig", function () {
expect(
() =>
new AppConfig({
// @ts-expect-error
foo: "bar",
}),
).toThrow();

View file

@ -72,7 +72,7 @@ export class AppConfig {
imageSize: imageSizeFieldSchema.parse(undefined),
};
constructor(initialData?: RootConfig) {
constructor(initialData?: RootConfig | unknown) {
if (initialData) {
try {
this.rootData = rootAppConfigSchema.parse(initialData);

View file

@ -2,6 +2,7 @@ import { router } from "../trpc/trpc-server";
import { protectedClientProcedure } from "../trpc/protected-client-procedure";
import { createLogger } from "@saleor/apps-shared";
import { updateCacheForConfigurations } from "../metadata-cache/update-cache-for-configurations";
import { AppConfigSchema, imageSizeInputSchema, titleTemplateInputSchema } from "./app-config";
import { z } from "zod";
import { createS3ClientFromConfiguration } from "../file-storage/s3/create-s3-client-from-configuration";
@ -105,6 +106,17 @@ export const appConfigurationRouter = router({
}) => {
const config = await getConfig();
/**
* TODO Check if this has to run, once its cached, it should be invalidated by webhooks only.
*
* But this operation isn't expensive and users will not continuously save this form
*/
await updateCacheForConfigurations({
client: apiClient,
channelsSlugs: [input.channelSlug],
saleorApiUrl: saleorApiUrl,
});
logger.debug({ channel: input.channelSlug }, "Updated cache for channel");
config.setChannelUrls(input.channelSlug, input.urls);

View file

@ -0,0 +1,37 @@
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { z } from "zod";
export class CacheConfigurator {
private metadataKeyPrefix = "cursor-cache-";
constructor(
private metadataManager: SettingsManager,
private saleorApiUrl: string,
) {}
private constructKey(channel: string) {
return this.metadataKeyPrefix + channel;
}
get({ channel }: { channel: string }): Promise<string[] | undefined> {
return this.metadataManager.get(this.constructKey(channel), this.saleorApiUrl).then((data) => {
if (!data) {
return undefined;
}
try {
return z.array(z.string()).parse(JSON.parse(data));
} catch (e) {
throw new Error("Invalid metadata value, can't be parsed");
}
});
}
set({ channel, value }: { channel: string; value: string[] }): Promise<void> {
return this.metadataManager.set({
key: this.constructKey(channel),
value: JSON.stringify(value),
domain: this.saleorApiUrl,
});
}
}

View file

@ -0,0 +1,33 @@
import { createLogger } from "@saleor/apps-shared";
import { CacheConfigurator } from "./cache-configurator";
import { createSettingsManager } from "../../lib/metadata-manager";
import { getCursors } from "../google-feed/fetch-product-data";
import { Client } from "urql";
interface UpdateCacheForConfigurationsArgs {
client: Client;
saleorApiUrl: string;
channelsSlugs: string[];
}
export const updateCacheForConfigurations = async ({
client,
channelsSlugs,
saleorApiUrl,
}: UpdateCacheForConfigurationsArgs) => {
const logger = createLogger({ saleorApiUrl: saleorApiUrl });
logger.debug("Updating the cursor cache");
const cache = new CacheConfigurator(createSettingsManager(client), saleorApiUrl);
const cacheUpdatePromises = channelsSlugs.map(async (channel) => {
const cursors = await getCursors({ client, channel });
await cache.set({ channel, value: cursors });
});
await Promise.all(cacheUpdatePromises);
logger.debug("Cursor cache updated");
};

View file

@ -0,0 +1,41 @@
import { GraphqlClientFactory } from "../../lib/create-graphql-client";
import { updateCacheForConfigurations } from "./update-cache-for-configurations";
import { AuthData } from "@saleor/app-sdk/APL";
import {
ProductVariantWebhookPayloadFragment,
ProductWebhookPayloadFragment,
} from "../../../generated/graphql";
import { NextApiResponse } from "next";
type ChannelFragment =
| Pick<ProductWebhookPayloadFragment, "channel" | "channelListings">
| Pick<ProductVariantWebhookPayloadFragment, "channel" | "channelListings">;
export const updateCacheOnWebhook = async ({
channels,
authData,
res,
}: {
authData: AuthData;
channels: ChannelFragment;
res: NextApiResponse;
}) => {
const client = GraphqlClientFactory.fromAuthData(authData);
const channelsSlugs = [
channels.channel,
...(channels.channelListings?.map((cl) => cl.channel.slug) ?? []),
].filter((c) => c) as string[];
if (channelsSlugs.length === 0) {
return res.status(200).end();
}
await updateCacheForConfigurations({
channelsSlugs,
client,
saleorApiUrl: authData.saleorApiUrl,
});
return res.status(200).end();
};

View file

@ -6,6 +6,9 @@ import { fetchProductData } from "../../../../../modules/google-feed/fetch-produ
import { GoogleFeedSettingsFetcher } from "../../../../../modules/google-feed/get-google-feed-settings";
import { generateGoogleXmlFeed } from "../../../../../modules/google-feed/generate-google-xml-feed";
import { fetchShopData } from "../../../../../modules/google-feed/fetch-shop-data";
import { CacheConfigurator } from "../../../../../modules/metadata-cache/cache-configurator";
import { createSettingsManager } from "../../../../../lib/metadata-manager";
import { GraphqlClientFactory } from "../../../../../lib/create-graphql-client";
import { uploadFile } from "../../../../../modules/file-storage/s3/upload-file";
import { createS3ClientFromConfiguration } from "../../../../../modules/file-storage/s3/create-s3-client-from-configuration";
import { getFileDetails } from "../../../../../modules/file-storage/s3/get-file-details";
@ -13,10 +16,6 @@ import { getDownloadUrl, getFileName } from "../../../../../modules/file-storage
import { RootConfig } from "../../../../../modules/app-configuration/app-config";
import { z, ZodError } from "zod";
export const config = {
maxDuration: 5 * 60, // 5 minutes
};
// By default we cache the feed for 5 minutes. This can be changed by setting the FEED_CACHE_MAX_AGE
const FEED_CACHE_MAX_AGE = process.env.FEED_CACHE_MAX_AGE
? parseInt(process.env.FEED_CACHE_MAX_AGE, 10)
@ -158,10 +157,23 @@ export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
logger.debug("Generating a new feed");
const cacheClient = GraphqlClientFactory.fromAuthData(authData);
if (!cacheClient) {
logger.error("Can't create the gql client");
return res.status(500).end();
}
// get cached cursors
const cache = new CacheConfigurator(createSettingsManager(cacheClient), authData.saleorApiUrl);
const cursors = await cache.get({ channel });
// TODO: instead of separate variants, use group id https://support.google.com/merchants/answer/6324507?hl=en
let productVariants: GoogleFeedProductVariantFragment[] = [];
try {
productVariants = await fetchProductData({ client, channel, imageSize });
productVariants = await fetchProductData({ client, channel, cursors, imageSize });
} catch (error) {
logger.error(error);
return res.status(400).end();

View file

@ -2,6 +2,11 @@ import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
import { AppManifest } from "@saleor/app-sdk/types";
import packageJson from "../../../package.json";
import { webhookProductCreated } from "./webhooks/product_created";
import { webhookProductDeleted } from "./webhooks/product_deleted";
import { webhookProductVariantCreated } from "./webhooks/product_variant_created";
import { webhookProductVariantDeleted } from "./webhooks/product_variant_deleted";
import { webhookProductVariantUpdated } from "./webhooks/product_variant_updated";
export default createManifestHandler({
async manifestFactory({ appBaseUrl }) {
@ -26,7 +31,13 @@ export default createManifestHandler({
supportUrl: "https://github.com/saleor/apps/discussions",
tokenTargetUrl: `${apiBaseURL}/api/register`,
version: packageJson.version,
webhooks: [],
webhooks: [
webhookProductCreated.getWebhookManifest(apiBaseURL),
webhookProductDeleted.getWebhookManifest(apiBaseURL),
webhookProductVariantCreated.getWebhookManifest(apiBaseURL),
webhookProductVariantDeleted.getWebhookManifest(apiBaseURL),
webhookProductVariantUpdated.getWebhookManifest(apiBaseURL),
],
};
return manifest;

View file

@ -0,0 +1,41 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { createLogger } from "@saleor/apps-shared";
import { saleorApp } from "../../../saleor-app";
import {
ProductCreatedDocument,
ProductWebhookPayloadFragment,
} from "../../../../generated/graphql";
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
export const config = {
api: {
bodyParser: false,
},
};
export const webhookProductCreated = new SaleorAsyncWebhook<ProductWebhookPayloadFragment>({
webhookPath: "api/webhooks/product_created",
event: "PRODUCT_CREATED",
apl: saleorApp.apl,
query: ProductCreatedDocument,
// todo make it disabled by default, enable when app is configured
isActive: true,
});
const logger = createLogger({
service: "webhook-product_created",
});
export const handler: NextWebhookApiHandler<ProductWebhookPayloadFragment> = async (
req,
res,
context
) => {
await updateCacheOnWebhook({
authData: context.authData,
channels: context.payload,
res,
});
};
export default webhookProductCreated.createHandler(handler);

View file

@ -0,0 +1,40 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import {
ProductDeletedDocument,
ProductWebhookPayloadFragment,
} from "../../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
import { createLogger } from "@saleor/apps-shared";
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
export const config = {
api: {
bodyParser: false,
},
};
export const webhookProductDeleted = new SaleorAsyncWebhook<ProductWebhookPayloadFragment>({
webhookPath: "api/webhooks/product_deleted",
event: "PRODUCT_DELETED",
apl: saleorApp.apl,
query: ProductDeletedDocument,
isActive: true,
});
const logger = createLogger({
service: "webhook_product_deleted",
});
export const handler: NextWebhookApiHandler<ProductWebhookPayloadFragment> = async (
req,
res,
context
) => {
await updateCacheOnWebhook({
authData: context.authData,
channels: context.payload,
res,
});
};
export default webhookProductDeleted.createHandler(handler);

View file

@ -0,0 +1,40 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import {
ProductUpdatedDocument,
ProductWebhookPayloadFragment,
} from "../../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
import { createLogger } from "@saleor/apps-shared";
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
export const config = {
api: {
bodyParser: false,
},
};
export const webhookProductUpdated = new SaleorAsyncWebhook<ProductWebhookPayloadFragment>({
webhookPath: "api/webhooks/product_updated",
event: "PRODUCT_UPDATED",
apl: saleorApp.apl,
query: ProductUpdatedDocument,
isActive: true,
});
const logger = createLogger({
service: "webhookProductUpdatedWebhookHandler",
});
export const handler: NextWebhookApiHandler<ProductWebhookPayloadFragment> = async (
req,
res,
context
) => {
await updateCacheOnWebhook({
authData: context.authData,
channels: context.payload,
res,
});
};
export default webhookProductUpdated.createHandler(handler);

View file

@ -0,0 +1,41 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import {
ProductVariantCreatedDocument,
ProductVariantWebhookPayloadFragment,
} from "../../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
import { createLogger } from "@saleor/apps-shared";
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
export const config = {
api: {
bodyParser: false,
},
};
export const webhookProductVariantCreated =
new SaleorAsyncWebhook<ProductVariantWebhookPayloadFragment>({
webhookPath: "api/webhooks/product_variant_created",
event: "PRODUCT_VARIANT_CREATED",
apl: saleorApp.apl,
query: ProductVariantCreatedDocument,
isActive: true,
});
const logger = createLogger({
service: "PRODUCT_VARIANT_CREATED webhook",
});
export const handler: NextWebhookApiHandler<ProductVariantWebhookPayloadFragment> = async (
req,
res,
context
) => {
await updateCacheOnWebhook({
authData: context.authData,
channels: context.payload,
res,
});
};
export default webhookProductVariantCreated.createHandler(handler);

View file

@ -0,0 +1,41 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { createLogger } from "@saleor/apps-shared";
import {
ProductVariantDeletedDocument,
ProductVariantWebhookPayloadFragment,
} from "../../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
export const config = {
api: {
bodyParser: false,
},
};
export const webhookProductVariantDeleted =
new SaleorAsyncWebhook<ProductVariantWebhookPayloadFragment>({
webhookPath: "api/webhooks/product_variant_deleted",
event: "PRODUCT_VARIANT_DELETED",
apl: saleorApp.apl,
query: ProductVariantDeletedDocument,
isActive: true,
});
const logger = createLogger({
service: "PRODUCT_VARIANT_DELETED",
});
export const handler: NextWebhookApiHandler<ProductVariantWebhookPayloadFragment> = async (
req,
res,
context
) => {
await updateCacheOnWebhook({
authData: context.authData,
channels: context.payload,
res,
});
};
export default webhookProductVariantDeleted.createHandler(handler);

View file

@ -0,0 +1,41 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { createLogger } from "@saleor/apps-shared";
import {
ProductVariantUpdatedDocument,
ProductVariantWebhookPayloadFragment,
} from "../../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
export const config = {
api: {
bodyParser: false,
},
};
export const webhookProductVariantUpdated =
new SaleorAsyncWebhook<ProductVariantWebhookPayloadFragment>({
webhookPath: "api/webhooks/product_variant_updated",
event: "PRODUCT_VARIANT_UPDATED",
apl: saleorApp.apl,
query: ProductVariantUpdatedDocument,
isActive: true,
});
const logger = createLogger({
service: "webhookProductVariantUpdatedWebhookHandler",
});
export const handler: NextWebhookApiHandler<ProductVariantWebhookPayloadFragment> = async (
req,
res,
context
) => {
await updateCacheOnWebhook({
authData: context.authData,
channels: context.payload,
res,
});
};
export default webhookProductVariantUpdated.createHandler(handler);

View file

@ -1,50 +1,5 @@
# saleor-app-search
## 1.16.0
### Minor Changes
- 1982d81: Added a new `pricing` field to the Algolia object representation. It now passes variant pricing representation from GraphQL:
```graphQL
price {
gross {
amount
}
net {
amount
}
}
discount {
gross {
amount
}
net {
amount
}
}
onSale
priceUndiscounted {
gross {
amount
}
net {
amount
}
}
```
### Patch Changes
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
- 7e0755e: Webhook migration scripts has been moved to the shared package.
- Updated dependencies [5dee65a]
- Updated dependencies [7e0755e]
- @saleor/webhook-utils@0.0.1
## 1.15.0
### Minor Changes

View file

@ -10,26 +10,7 @@ fragment ProductVariantData on ProductVariant {
price {
gross {
amount
}
net {
amount
}
}
discount {
gross {
amount
}
net {
amount
}
}
onSale
priceUndiscounted {
gross {
amount
}
net {
amount
currency
}
}
}

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-search",
"version": "1.16.0",
"version": "1.15.0",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -12,7 +12,7 @@
"test": "vitest"
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@hookform/resolvers": "^3.1.0",
"@saleor/app-sdk": "0.43.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
@ -44,7 +44,7 @@
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/cli": "4.0.1",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/schema-ast": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
@ -52,6 +52,7 @@
"@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/typescript-urql": "3.7.3",
"@graphql-typed-document-node/core": "3.2.0",
"@total-typescript/ts-reset": "^0.5.1",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",
"@vitejs/plugin-react": "4.0.4",

1
apps/search/reset.d.ts vendored Normal file
View file

@ -0,0 +1 @@
import "@total-typescript/ts-reset";

View file

@ -78,15 +78,9 @@ export class AlgoliaSearchProvider implements SearchProvider {
"productId",
"inStock",
"categories",
"grossPrice",
"attributes",
"collections",
"pricing.price.net",
"pricing.price.gross",
"pricing.discount.net",
"pricing.discount.gross",
"pricing.priceUndiscounted.net",
"pricing.priceUndiscounted.gross",
"pricing.onSale",
],
attributeForDistinct: "productId",
numericAttributesForFiltering: ["grossPrice"],

View file

@ -134,25 +134,7 @@ export function productAndVariantToAlgolia({
descriptionPlaintext: EditorJsPlaintextRenderer({ stringData: product.description }),
slug: product.slug,
thumbnail: product.thumbnail?.url,
/**
* Deprecated
*/
grossPrice: listing?.price?.amount,
pricing: {
price: {
net: variant.pricing?.price?.net.amount,
gross: variant.pricing?.price?.gross.amount,
},
onSale: variant.pricing?.onSale,
discount: {
net: variant.pricing?.discount?.net.amount,
gross: variant.pricing?.discount?.gross.amount,
},
priceUndiscounted: {
net: variant.pricing?.priceUndiscounted?.net.amount,
gross: variant.pricing?.priceUndiscounted?.gross.amount,
},
},
inStock,
categories: categoryHierarchicalFacets(variant),
collections: product.collections?.map((collection) => collection.name) || [],

View file

@ -7,7 +7,9 @@ export const useWebhooksStatus = () => {
const fetch: typeof window.fetch = useAuthenticatedFetch();
const fetchFn = useCallback(() => {
return fetch("/api/webhooks-status").then((resp) => resp.json());
return fetch("/api/webhooks-status").then(
(resp) => resp.json() as unknown as WebhooksStatusResponse,
);
/**
* fetch from SDK is not wrapped with memo todo
*/

View file

@ -27,7 +27,7 @@ export class AppConfig {
},
};
constructor(initialData?: AppConfigRootSchemaFields) {
constructor(initialData?: AppConfigRootSchemaFields | unknown) {
if (initialData) {
this.rootData = AppConfigRootSchema.parse(initialData);
}

View file

@ -1,4 +1,4 @@
import { NextProtectedApiHandler } from "@saleor/app-sdk/handlers/next";
import { NextProtectedApiHandler, ProtectedHandlerContext } from "@saleor/app-sdk/handlers/next";
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { createMocks } from "node-mocks-http";
import { Client, OperationResult } from "urql";
@ -8,11 +8,12 @@ import { IWebhookActivityTogglerService } from "../../domain/WebhookActivityTogg
import { SearchProvider } from "../../lib/searchProvider";
import { webhooksStatusHandlerFactory } from "../../pages/api/webhooks-status";
import { AppConfig } from "../../modules/configuration/configuration";
import { NextApiRequest, NextApiResponse } from "next";
/**
* Context provided from ProtectedApiHandler to handler body
*/
const mockWebhookContext = {
const mockWebhookContext: ProtectedHandlerContext = {
authData: {
appId: "app-id",
domain: "domain.saleor.io",
@ -20,6 +21,10 @@ const mockWebhookContext = {
saleorApiUrl: "https://domain.saleor.io/graphql",
},
baseUrl: "localhost:3000",
user: {
email: "",
userPermissions: [],
},
};
const appWebhooksResponseData: Pick<OperationResult<FetchOwnWebhooksQuery, any>, "data"> = {
@ -90,6 +95,7 @@ describe("webhooksStatusHandler", () => {
it("Disables webhooks if Algolia settings are not saved in Saleor Metadata", async function () {
const { req, res } = createMocks({});
// @ts-expect-error - mock doesnt contain next-specific fields
await handler(req, res, mockWebhookContext);
expect(webhooksTogglerServiceMock.disableOwnWebhooks).toHaveBeenCalled();
@ -113,6 +119,7 @@ describe("webhooksStatusHandler", () => {
const { req, res } = createMocks({});
// @ts-expect-error - mock doesnt contain next-specific fields
await handler(req, res, mockWebhookContext);
expect(webhooksTogglerServiceMock.disableOwnWebhooks).toHaveBeenCalled();

View file

@ -1,14 +1,5 @@
# saleor-app-segment
## 1.0.3
### Patch Changes
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 1.0.2
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-segment",
"version": "1.0.3",
"version": "1.0.2",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -12,7 +12,7 @@
"test": "vitest"
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@hookform/resolvers": "^3.1.0",
"@saleor/app-sdk": "0.43.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
@ -45,7 +45,7 @@
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/cli": "4.0.1",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
"@graphql-codegen/typescript": "4.0.1",
@ -54,6 +54,7 @@
"@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@total-typescript/ts-reset": "^0.5.1",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",
"eslint": "8.46.0",

1
apps/segment/reset.d.ts vendored Normal file
View file

@ -0,0 +1 @@
import "@total-typescript/ts-reset";

Some files were not shown because too many files have changed in this diff Show more