Compare commits

...

1 commit

Author SHA1 Message Date
Lukasz Ostrowski
204ab41168 Add POC / example of managing app state 2023-04-18 23:21:08 +02:00
6 changed files with 4567 additions and 4050 deletions

View file

@ -62,6 +62,7 @@
"@types/react": "^18.0.27", "@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10", "@types/react-dom": "^18.0.10",
"@types/rimraf": "^3.0.2", "@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.13",
"@vitejs/plugin-react": "^3.0.0", "@vitejs/plugin-react": "^3.0.0",
"@vitest/coverage-c8": "^0.28.4", "@vitest/coverage-c8": "^0.28.4",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
@ -70,7 +71,6 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"typescript": "4.9.5", "typescript": "4.9.5",
"vite": "^4.2.1", "vite": "^4.2.1",
"vitest": "^0.30.1", "vitest": "^0.30.1"
"@types/semver": "^7.3.13"
} }
} }

View file

@ -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":{}}`);
});
});

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

View file

@ -13,7 +13,8 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true "incremental": true,
"experimentalDecorators": true
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff