Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type Market #95

Merged
merged 12 commits into from
Apr 17, 2024
42 changes: 42 additions & 0 deletions apps/server/migrations/0017_pv_type_market.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { type Kysely } from "kysely";

export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable("part_variation_type")
.addColumn("id", "text", (col) => col.primaryKey())
.addColumn("workspace_id", "text", (col) =>
col.notNull().references("workspace.id").onDelete("cascade"),
)
.addColumn("name", "text", (col) => col.notNull())
.execute();

await db.schema
.createTable("part_variation_market")
.addColumn("id", "text", (col) => col.primaryKey())
.addColumn("workspace_id", "text", (col) =>
col.notNull().references("workspace.id").onDelete("cascade"),
)
.addColumn("name", "text", (col) => col.notNull())
.execute();

await db.schema
.alterTable("part_variation")
.addColumn("type_id", "text", (col) =>
col.references("part_variation_type.id"),
)
.addColumn("market_id", "text", (col) =>
col.references("part_variation_market.id"),
)
.execute();
}

export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema
.alterTable("part_variation")
.dropColumn("type_id")
.dropColumn("market_id")
.execute();

await db.schema.dropTable("part_variation_type").execute();
await db.schema.dropTable("part_variation_market").execute();
}
119 changes: 117 additions & 2 deletions apps/server/src/db/part-variation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
PartVariationTreeNode,
PartVariationTreeRoot,
} from "@cloud/shared";
import { Kysely } from "kysely";
import { ExpressionBuilder, Kysely } from "kysely";
import { Result, err, ok } from "neverthrow";
import { markUpdatedAt } from "../db/query";
import { generateDatabaseId } from "../lib/db-utils";
Expand All @@ -17,6 +17,75 @@ import {
BadRequestError,
RouteError,
} from "../lib/error";
import { PartVariationType } from "@cloud/shared/src/schemas/public/PartVariationType";
import { jsonObjectFrom } from "kysely/helpers/postgres";

async function getOrCreateType(
db: Kysely<DB>,
typeName: string,
workspaceId: string,
): Promise<Result<PartVariationType, RouteError>> {
const type = await db
.selectFrom("part_variation_type as pvt")
.selectAll("pvt")
.where("pvt.name", "=", typeName)
.where("pvt.workspaceId", "=", workspaceId)
.executeTakeFirst();

if (type) {
return ok(type);
}
const insertResult = await db
.insertInto("part_variation_type")
.values({
id: generateDatabaseId("part_variation_type"),
workspaceId,
name: typeName,
})
.returningAll()
.executeTakeFirst();

if (!insertResult) {
return err(new InternalServerError("Failed to create part variation type"));
}

return ok(insertResult);
}

async function getOrCreateMarket(
db: Kysely<DB>,
marketName: string,
workspaceId: string,
): Promise<Result<PartVariationType, RouteError>> {
const market = await db
.selectFrom("part_variation_market as pvt")
.selectAll("pvt")
.where("pvt.name", "=", marketName)
.where("pvt.workspaceId", "=", workspaceId)
.executeTakeFirst();

if (market) {
return ok(market);
}

const insertResult = await db
.insertInto("part_variation_market")
.values({
id: generateDatabaseId("part_variation_market"),
workspaceId,
name: marketName,
})
.returningAll()
.executeTakeFirst();

if (!insertResult) {
return err(
new InternalServerError("Failed to create part variation market"),
);
}

return ok(insertResult);
}

export async function createPartVariation(
db: Kysely<DB>,
Expand All @@ -36,12 +105,35 @@ export async function createPartVariation(
),
);
}
const { type: typeName, market: marketName, ...data } = newPartVariation;

let typeId: string | undefined = undefined;
let marketId: string | undefined = undefined;

if (typeName) {
const type = await getOrCreateType(db, typeName, input.workspaceId);
if (type.isOk()) {
typeId = type.value.id;
} else {
return err(type.error);
}
}
if (marketName) {
const market = await getOrCreateMarket(db, marketName, input.workspaceId);
if (market.isOk()) {
marketId = market.value.id;
} else {
return err(market.error);
}
}

const partVariation = await db
.insertInto("part_variation")
.values({
id: generateDatabaseId("part_variation"),
...newPartVariation,
...data,
typeId,
marketId,
})
.returningAll()
.executeTakeFirst();
Expand Down Expand Up @@ -74,6 +166,7 @@ export async function getPartVariation(partVariationId: string) {
.selectFrom("part_variation")
.selectAll()
.where("part_variation.id", "=", partVariationId)

.executeTakeFirst();
}

Expand Down Expand Up @@ -164,3 +257,25 @@ function buildPartVariationTree(

return root;
}

export function withPartVariationType(
eb: ExpressionBuilder<DB, "part_variation">,
) {
return jsonObjectFrom(
eb
.selectFrom("part_variation_type as pvt")
.selectAll("pvt")
.whereRef("pvt.id", "=", "part_variation.typeId"),
).as("type");
}

export function withPartVariationMarket(
eb: ExpressionBuilder<DB, "part_variation">,
) {
return jsonObjectFrom(
eb
.selectFrom("part_variation_market as pvm")
.selectAll("pvm")
.whereRef("pvm.id", "=", "part_variation.marketId"),
).as("market");
}
26 changes: 23 additions & 3 deletions apps/server/src/routes/part-variation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { db } from "../db/kysely";
import {
createPartVariation,
getPartVariationTree,
withPartVariationMarket,
withPartVariationType,
} from "../db/part-variation";
import { withUnitParent } from "../db/unit";
import { checkWorkspacePerm } from "../lib/perm/workspace";
Expand All @@ -22,11 +24,27 @@ export const PartVariationRoute = new Elysia({
.where("part_variation.workspaceId", "=", workspace.id)
.leftJoin("unit", "part_variation.id", "unit.partVariationId")
.select(({ fn }) => fn.count<number>("unit.id").as("unitCount"))
.select((eb) => [withPartVariationType(eb)])
.select((eb) => [withPartVariationMarket(eb)])
.groupBy("part_variation.id")
.execute();

return partVariations;
})
.get("/type", async ({ workspace }) => {
return await db
.selectFrom("part_variation_type as pvt")
.selectAll("pvt")
.where("pvt.workspaceId", "=", workspace.id)
.execute();
})
.get("/market", async ({ workspace }) => {
return await db
.selectFrom("part_variation_market as pvm")
.selectAll("pvm")
.where("pvm.workspaceId", "=", workspace.id)
.execute();
})
.post(
"/",
async ({ body, error }) => {
Expand Down Expand Up @@ -56,9 +74,11 @@ export const PartVariationRoute = new Elysia({
async ({ workspace, params: { partVariationId }, error }) => {
const partVariation = await db
.selectFrom("part_variation")
.selectAll()
.where("id", "=", partVariationId)
.where("workspaceId", "=", workspace.id)
.selectAll("part_variation")
.where("part_variation.id", "=", partVariationId)
.where("part_variation.workspaceId", "=", workspace.id)
.select((eb) => [withPartVariationType(eb)])
.select((eb) => [withPartVariationMarket(eb)])
.executeTakeFirst();

if (partVariation === undefined) {
Expand Down
7 changes: 7 additions & 0 deletions apps/server/src/routes/part.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { insertPart } from "@cloud/shared";
import { WorkspaceMiddleware } from "../middlewares/workspace";
import { checkPartPerm } from "../lib/perm/part";
import { checkWorkspacePerm } from "../lib/perm/workspace";
import {
withPartVariationMarket,
withPartVariationType,
} from "../db/part-variation";

export const PartRoute = new Elysia({ prefix: "/part", name: "PartRoute" })
.use(WorkspaceMiddleware)
Expand Down Expand Up @@ -38,6 +42,7 @@ export const PartRoute = new Elysia({ prefix: "/part", name: "PartRoute" })
.where("part.id", "=", partId)
.innerJoin("product", "product.id", "part.productId")
.select("product.name as productName")

.executeTakeFirst();
if (part === undefined) return error(404, "Part not found");
return part;
Expand Down Expand Up @@ -65,6 +70,8 @@ export const PartRoute = new Elysia({ prefix: "/part", name: "PartRoute" })
.selectAll("part_variation")
.where("part_variation.workspaceId", "=", workspace.id)
.where("part_variation.partId", "=", partId)
.select((eb) => [withPartVariationType(eb)])
.select((eb) => [withPartVariationMarket(eb)])
.leftJoin("unit", "part_variation.id", "unit.partVariationId")
.select(({ fn }) => fn.count<number>("unit.id").as("unitCount"))
.groupBy("part_variation.id")
Expand Down
53 changes: 53 additions & 0 deletions apps/web/src/components/unit/create-part-variation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ import { useCallback, useEffect } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { Checkbox } from "../ui/checkbox";
import { Autocomplete } from "../ui/autocomplete";
import { PartVariationType } from "@cloud/shared/src/schemas/public/PartVariationType";
import { PartVariationMarket } from "@cloud/shared/src/schemas/public/PartVariationMarket";

const partVariationFormSchema = t.Composite([
insertPartVariation,
Expand All @@ -65,6 +68,8 @@ type Props = {
open: boolean;
setOpen: (open: boolean) => void;
openDialog: () => void;
partVariationTypes: PartVariationType[];
partVariationMarkets: PartVariationMarket[];
};

const CreatePartVariation = ({
Expand All @@ -76,6 +81,8 @@ const CreatePartVariation = ({
openDialog,
defaultValues,
setDefaultValues,
partVariationTypes,
partVariationMarkets,
}: Props) => {
const queryClient = useQueryClient();

Expand Down Expand Up @@ -255,6 +262,52 @@ const CreatePartVariation = ({
</FormItem>
)}
/>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Type</FormLabel>
<FormControl>
<Autocomplete
options={partVariationTypes.map((p) => p.name)}
{...field}
value={field.value ?? ""}
onChange={(val) => form.setValue("type", val)}
placeholder="Search or create new..."
data-1p-ignore
/>
</FormControl>
<FormDescription>
(Optional) What type of part is this? (e.g. PCB)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="market"
render={({ field }) => (
<FormItem>
<FormLabel>Market</FormLabel>
<FormControl>
<Autocomplete
options={partVariationMarkets.map((p) => p.name)}
{...field}
value={field.value ?? ""}
onChange={(val) => form.setValue("market", val)}
placeholder="Search or create new..."
data-1p-ignore
/>
</FormControl>
<FormDescription>
(Optional) The targeting market of this part. (e.g. Medical)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="hasComponents"
Expand Down
Loading
Loading