Add POC / example of managing app state
This commit is contained in:
parent
a3636f73ef
commit
204ab41168
6 changed files with 4567 additions and 4050 deletions
|
@ -62,6 +62,7 @@
|
|||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/semver": "^7.3.13",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"@vitest/coverage-c8": "^0.28.4",
|
||||
"dotenv": "^16.0.3",
|
||||
|
@ -70,7 +71,6 @@
|
|||
"rimraf": "^3.0.2",
|
||||
"typescript": "4.9.5",
|
||||
"vite": "^4.2.1",
|
||||
"vitest": "^0.30.1",
|
||||
"@types/semver": "^7.3.13"
|
||||
"vitest": "^0.30.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import { describe, test, expect } from "vitest";
|
||||
import { ConfigModel } from "./config-v3";
|
||||
|
||||
describe("configv3", () => {
|
||||
test("Constructs", () => {
|
||||
const instance = new ConfigModel();
|
||||
|
||||
expect(instance).toBeDefined();
|
||||
});
|
||||
|
||||
test("Serializes", () => {
|
||||
const instance = new ConfigModel({
|
||||
overrides: {
|
||||
usd: {
|
||||
channel: {
|
||||
slug: "usd",
|
||||
},
|
||||
address: {
|
||||
city: "Krakow",
|
||||
cityArea: "krowodrza",
|
||||
country: "poland",
|
||||
streetAddress1: "Some street",
|
||||
streetAddress2: "",
|
||||
postalCode: "12345",
|
||||
companyName: "Saleor",
|
||||
countryArea: "Malopolskie",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(instance.serialize()).toEqual(
|
||||
'{"overrides":{"usd":{"channel":{"slug":"usd"},"address":{"city":"Krakow","cityArea":"krowodrza","country":"poland","streetAddress1":"Some street","streetAddress2":"","postalCode":"12345","companyName":"Saleor","countryArea":"Malopolskie"}}}}'
|
||||
);
|
||||
});
|
||||
|
||||
test("Parses root schema", () => {
|
||||
const instance = ConfigModel.parse(
|
||||
'{"overrides":{"usd":{"channel":{"slug":"usd"},"address":{"city":"Krakow","cityArea":"krowodrza","country":"poland","streetAddress1":"Some street","streetAddress2":"","postalCode":"12345","companyName":"Saleor","countryArea":"Malopolskie"}}}}'
|
||||
);
|
||||
|
||||
expect(instance.getOverridesArray()).toHaveLength(1);
|
||||
expect(instance.getOverridesArray()[0].channel.slug).toEqual("usd");
|
||||
});
|
||||
|
||||
test("Appends override", () => {
|
||||
const instance = new ConfigModel();
|
||||
|
||||
expect(instance.getOverridesArray()).toHaveLength(0);
|
||||
|
||||
instance.addOverride("usd_USD", {
|
||||
city: "Krakow",
|
||||
cityArea: "krowodrza",
|
||||
country: "poland",
|
||||
streetAddress1: "Some street",
|
||||
streetAddress2: "",
|
||||
postalCode: "12345",
|
||||
companyName: "Saleor",
|
||||
countryArea: "Malopolskie",
|
||||
});
|
||||
|
||||
expect(instance.getOverridesArray()).toHaveLength(1);
|
||||
expect(instance.getOverridesArray()[0].channel.slug).toEqual("usd_USD");
|
||||
});
|
||||
|
||||
test("Removes override", () => {
|
||||
const instance = new ConfigModel({
|
||||
overrides: {
|
||||
usd: {
|
||||
channel: {
|
||||
slug: "usd",
|
||||
},
|
||||
address: {
|
||||
city: "Krakow",
|
||||
cityArea: "krowodrza",
|
||||
country: "poland",
|
||||
streetAddress1: "Some street",
|
||||
streetAddress2: "",
|
||||
postalCode: "12345",
|
||||
companyName: "Saleor",
|
||||
countryArea: "Malopolskie",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
instance.removeOverride("usd");
|
||||
|
||||
expect(instance.getOverridesArray()).toHaveLength(0);
|
||||
|
||||
expect(instance.serialize()).toEqual(`{"overrides":{}}`);
|
||||
});
|
||||
});
|
137
apps/invoices/src/modules/app-configuration/config-v3.ts
Normal file
137
apps/invoices/src/modules/app-configuration/config-v3.ts
Normal file
|
@ -0,0 +1,137 @@
|
|||
import { z } from "zod";
|
||||
import { test } from "vitest";
|
||||
import { EncryptedMetadataManager, SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||
|
||||
const AddressSchema = z.object({
|
||||
companyName: z.string(),
|
||||
cityArea: z.string(),
|
||||
countryArea: z.string(),
|
||||
streetAddress1: z.string(),
|
||||
streetAddress2: z.string(),
|
||||
postalCode: z.string(),
|
||||
city: z.string(),
|
||||
country: z.string(),
|
||||
});
|
||||
|
||||
const ChannelSchema = z.object({
|
||||
slug: z.string().min(1),
|
||||
});
|
||||
|
||||
const AddressOverrideSchema = z.object({
|
||||
address: AddressSchema,
|
||||
channel: ChannelSchema,
|
||||
});
|
||||
|
||||
const RootConfigSchema = z.object({
|
||||
overrides: z.record(AddressOverrideSchema),
|
||||
});
|
||||
|
||||
/**
|
||||
* Root model that can serialize to json and parse from json.
|
||||
*
|
||||
* Uses Zod to parse and validate
|
||||
*
|
||||
* Adds domain methods on top
|
||||
*/
|
||||
export class ConfigModel {
|
||||
/**
|
||||
* Stores its own data as deep, single json, structured and validated by zod
|
||||
*/
|
||||
private rootData: z.infer<typeof RootConfigSchema> = { overrides: {} };
|
||||
|
||||
constructor(initialConfig?: z.infer<typeof RootConfigSchema>) {
|
||||
/**
|
||||
* Sets its own initial state but also allows to inject - then validate
|
||||
*/
|
||||
if (initialConfig) {
|
||||
this.rootData = RootConfigSchema.parse(initialConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can statically parse itself, ensures parse/serialize work together
|
||||
*/
|
||||
static parse(serialized: string) {
|
||||
return new ConfigModel(RootConfigSchema.parse(JSON.parse(serialized)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes to json, even if some extra methods are saved (can be replaced/cleaned up if needed),
|
||||
* zod will remove unknown members after parsing
|
||||
*/
|
||||
serialize() {
|
||||
return JSON.stringify(this.rootData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain methods needed by app
|
||||
*/
|
||||
getOverridesArray() {
|
||||
return Object.values(this.rootData.overrides);
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain methods needed by app
|
||||
*/
|
||||
isChannelOverridden(slug: string) {
|
||||
return Boolean(this.rootData.overrides[slug]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain methods needed by app
|
||||
*/
|
||||
addOverride(slug: string, address: z.infer<typeof AddressSchema>) {
|
||||
/**
|
||||
* Perform additional checks, for example implement "update" method and forbid to implicit override
|
||||
*/
|
||||
if (this.rootData.overrides[slug]) {
|
||||
throw new Error("Channel override already exists");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure input is correct. Use "satisfied" because zod accepts "unknown"
|
||||
*/
|
||||
this.rootData.overrides[slug] = AddressOverrideSchema.parse({
|
||||
channel: {
|
||||
slug: slug,
|
||||
},
|
||||
address: address,
|
||||
} satisfies z.infer<typeof AddressOverrideSchema>);
|
||||
|
||||
/**
|
||||
* Return this to allow chaining, optional
|
||||
*/
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain methods needed by app
|
||||
*/
|
||||
removeOverride(slug: string) {
|
||||
delete this.rootData.overrides[slug];
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* model can be connected with MetadataManager to automatically fetch and parse data.
|
||||
*
|
||||
* So for app usage this can be the only "root source" used
|
||||
*/
|
||||
abstract class ConfigManager {
|
||||
/**
|
||||
* Uses metadata manager to read/write
|
||||
*/
|
||||
abstract metadataManager: SettingsManager;
|
||||
|
||||
/**
|
||||
* Can fetch config and parse it to domain model
|
||||
*/
|
||||
abstract loadConfig(): Promise<ConfigModel>;
|
||||
|
||||
/**
|
||||
* Can serialize and save config in metadata
|
||||
*/
|
||||
abstract saveConfig(config: ConfigModel): Promise<void>;
|
||||
}
|
|
@ -13,7 +13,8 @@
|
|||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
"incremental": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
|
|
62
apps/taxes/playwright-report/index.html
Normal file
62
apps/taxes/playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
8318
pnpm-lock.yaml
8318
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue