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

Create public end Point to import pack template #1351

Merged
merged 4 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/validations/src/validations/itemRoutesValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,20 @@ export const addItemGlobal = z.object({
name: z.string(),
weight: z.number(),
unit: z.string(),
type: z.string(),
type: z.enum(['Food', 'Water', 'Essentials']),
ownerId: z.string(),
sku: z.string().optional(),
productUrl: z.string().optional(),
description: z.string().optional(),
productDetails: z
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
.optional(),
seller: z.string().optional(),
image_urls: z.string().optional(),
});

export type AddItemGlobalType = z.infer<typeof addItemGlobal>;

export const importItemsGlobal = z.object({
content: z.string(),
ownerId: z.string(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from 'zod';
import { addItemGlobal } from './itemRoutesValidator';

export const getPackTemplates = z.object({
filter: z
Expand All @@ -13,11 +14,35 @@ export const getPackTemplates = z.object({
}),
});

export const getPackTemplate = z.object({
id: z.string().min(1),
});
export const getPackTemplate = z.union([
z.object({
id: z.string().min(1),
name: z.string().optional(),
}),
z.object({
name: z.string(),
id: z.string().optional(),
}),
]);

export const createPackFromTemplate = z.object({
packTemplateId: z.string().min(1),
newPackName: z.string().min(1),
});

export const addPackTemplate = z.object({
name: z.string(),
description: z.string(),
type: z.string(),
itemsOwnerId: z.string(),
itemPackTemplates: z.array(
z.object({
item: addItemGlobal.omit({ ownerId: true }),
quantity: z.number(),
}),
),
});

export type AddPackTemplateType = z.infer<typeof addPackTemplate>;

export const addPackTemplates = z.array(addPackTemplate);
113 changes: 71 additions & 42 deletions server/src/controllers/item/importItemsGlobal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type Context } from 'hono';
import {
addItemGlobalService,
addItemGlobalServiceBatch,
bulkAddItemsGlobalService,
} from '../../services/item/item.service';
import { protectedProcedure } from '../../trpc';
import * as validator from '@packrat/validations';
Expand Down Expand Up @@ -72,6 +72,61 @@ export const importItemsGlobal = async (c: Context) => {
}
};

/**
* Converts a list of raw CSV items into an iterable of validated items.
* @param {Array<Record<string, unknown>>} csvRawItems - The raw CSV items.
* @param {string} ownerId - The ID of the owner.
* @returns {Iterable<validator.AddItemGlobalType>} An iterable that yields the validated items.
*/
function* sanitizeItemsIterator(
csvRawItems: Array<Record<string, unknown>>,
ownerId: string,
): Generator<validator.AddItemGlobalType> {
for (let idx = 0; idx < csvRawItems.length; idx++) {
const item = csvRawItems[idx];

const productDetailsStr = `${item.techs}`
.replace(/'([^']*)'\s*:/g, '"$1":') // Replace single quotes keys with double quotes.
.replace(/:\s*'([^']*)'/g, ': "$1"') // Replace single quotes values with double quotes.
.replace(/\\x([0-9A-Fa-f]{2})/g, (match, hex) => {
// Replace hex escape sequences with UTF-8 characters
const codePoint = parseInt(hex, 16);
return String.fromCharCode(codePoint);
});

console.log(`${idx} / ${csvRawItems.length}`);
let parsedProductDetails:
| validator.AddItemGlobalType['productDetails']
| null = null;
try {
parsedProductDetails = JSON.parse(productDetailsStr);
} catch (e) {
console.log(
`${productDetailsStr}\nFailed to parse product details for item ${item.Name}: ${e.message}`,
);
throw e;
}

const validatedItem: validator.AddItemGlobalType = {
name: String(item.Name),
weight: Number(item.Weight),
unit: String(item.Unit),
type: String(item.Category) as ItemCategoryEnum,
ownerId,
image_urls: item.image_urls && String(item.image_urls),
sku: item.sku && String(item.sku),
productUrl: item.product_url && String(item.product_url),
description: item.description && String(item.description),
seller: item.seller && String(item.seller),
};

if (parsedProductDetails) {
validatedItem.productDetails = parsedProductDetails;
}

yield validatedItem;
}
}
export function importItemsGlobalRoute() {
const expectedHeaders = [
'Name',
Expand Down Expand Up @@ -114,49 +169,23 @@ export function importItemsGlobalRoute() {
results.data.pop();
}

let idx = 0;
await addItemGlobalServiceBatch(
results.data,
(item) => {
const productDetailsStr = `${item.techs}`
.replace(/'([^']*)'\s*:/g, '"$1":') // Replace single quotes keys with double quotes.
.replace(/:\s*'([^']*)'/g, ': "$1"') // Replace single quotes values with double quotes.
.replace(/\\x([0-9A-Fa-f]{2})/g, (match, hex) => {
// Replace hex escape sequences with UTF-8 characters
const codePoint = parseInt(hex, 16);
return String.fromCharCode(codePoint);
});

idx++;
console.log(`${idx} / ${results.data.length}`);
try {
const parsedProductDetails = JSON.parse(productDetailsStr);
} catch (e) {
console.log(
`${productDetailsStr}\nFailed to parse product details for item ${item.Name}: ${e.message}`,
);
throw e;
}

return {
name: String(item.Name),
weight: Number(item.Weight),
unit: String(item.Unit),
type: String(item.Category) as ItemCategoryEnum,
ownerId,
executionCtx: opts.ctx.executionCtx,
image_urls: item.image_urls && String(item.image_urls),
sku: item.sku && String(item.sku),
productUrl: item.product_url && String(item.product_url),
description: item.description && String(item.description),
seller: item.seller && String(item.seller),
productDetails: JSON.parse(productDetailsStr),
};
},
false,
const errors: Error[] = [];
const createdItems = await bulkAddItemsGlobalService(
sanitizeItemsIterator(results.data, ownerId),
opts.ctx.executionCtx,
{
onItemCreationError: (error) => {
errors.push(error);
},
},
);
return resolve('items');

return resolve({
status: 'success',
items: createdItems,
errorsCount: errors.length,
errors,
});
} catch (error) {
console.error(error);
return reject(new Error(`Failed to add items: ${error.message}`));
Expand Down
38 changes: 38 additions & 0 deletions server/src/controllers/packTemplates/addPackTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { addPackTemplateService } from '../../services/packTemplate/packTemplate.service';
import * as validator from '@packrat/validations';
import { publicProcedure } from '../../trpc';

export function importPackTemplatesRoute() {
return publicProcedure
.input(validator.addPackTemplates)
.mutation(async (opts) => {
const array = opts.input;
const packTemplates = [];
for (let idx = 0; idx < array.length; idx++) {
const input = array[idx];
try {
const packTemplate = await addPackTemplateService(
input,
opts.ctx.executionCtx,
);
packTemplates.push(packTemplate);
} catch (error) {
console.log(error);
throw error;
}
}
return packTemplates;
});
}

export function addPackTemplateRoute() {
return publicProcedure
.input(validator.addPackTemplate)
.mutation(async (opts) => {
const packTemplate = await addPackTemplateService(
opts.input,
opts.ctx.executionCtx,
);
return packTemplate;
});
}
3 changes: 2 additions & 1 deletion server/src/controllers/packTemplates/getPackTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export function getPackTemplateRoute() {
return protectedProcedure
.input(validator.getPackTemplate)
.query(async ({ input }) => {
return await getPackTemplateService(input.id);
const param = input.id ? { id: input.id } : { name: input.name };
return await getPackTemplateService(param);
});
}
3 changes: 2 additions & 1 deletion server/src/controllers/packTemplates/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './addPackTemplate';
export * from './getPackTemplates';
export * from './getPackTemplate'
export * from './getPackTemplate';
export * from './createPackFromTemplate';
15 changes: 10 additions & 5 deletions server/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,10 @@ export const packTemplate = sqliteTable('pack_template', {
});

export const packTemplateRelations = relations(packTemplate, ({ many }) => ({
itemPackTemplates: many(itemPackTemplates),
itemPackTemplates: many(itemPackTemplate),
}));

export const itemPackTemplates = sqliteTable(
export const itemPackTemplate = sqliteTable(
'item_pack_templates',
{
itemId: text('item_id').references(() => item.id, { onDelete: 'cascade' }),
Expand All @@ -205,14 +205,14 @@ export const itemPackTemplates = sqliteTable(
);

export const itemPackTemplatesRelations = relations(
itemPackTemplates,
itemPackTemplate,
({ one }) => ({
packTemplate: one(packTemplate, {
fields: [itemPackTemplates.packTemplateId],
fields: [itemPackTemplate.packTemplateId],
references: [packTemplate.id],
}),
item: one(item, {
fields: [itemPackTemplates.itemId],
fields: [itemPackTemplate.itemId],
references: [item.id],
}),
}),
Expand Down Expand Up @@ -369,6 +369,7 @@ export const itemRelations = relations(item, ({ one, many }) => ({
images: many(itemImage),
itemOwners: many(itemOwners),
itemPacks: many(itemPacks),
itemPackTemplates: many(itemPackTemplate),
}));

export const template = sqliteTable('template', {
Expand Down Expand Up @@ -611,8 +612,12 @@ export const insertTemplateSchema = createInsertSchema(template);
export const selectTemplateSchema = createSelectSchema(template);

export type PackTemplate = InferSelectModel<typeof packTemplate>;
export type InsertPackTemplate = InferInsertModel<typeof packTemplate>;
export const selectPackTemplateSchema = createSelectSchema(packTemplate);

export type ItemPackTemplate = InferSelectModel<typeof itemPackTemplate>;
export type InsertItemPackTemplate = InferInsertModel<typeof itemPackTemplate>;

export type Pack = InferSelectModel<typeof pack>;
export type InsertPack = InferInsertModel<typeof pack>;
export const insertPackSchema = createInsertSchema(pack);
Expand Down
17 changes: 17 additions & 0 deletions server/src/drizzle/methods/Item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ITEM_TABLE_NAME,
itemPacks,
item as ItemTable,
itemImage as itemImageTable,
} from '../../db/schema';
import { scorePackService } from '../../services/pack/scorePackService';
import { ItemPacks } from './ItemPacks';
Expand All @@ -25,6 +26,22 @@ export class Item {
}
}

async insertImage(itemId: string, url: string) {
try {
const itemImage = await DbClient.instance
.insert(itemImageTable)
.values({
itemId,
url,
})
.run();

return itemImage;
} catch (error) {
throw new Error(`Failed to update item: ${error.message}`);
}
}

async createPackItem(data: InsertItem, packId: string, quantity: number) {
// TODO wrap in transaction
const item = await this.create(data);
Expand Down
39 changes: 39 additions & 0 deletions server/src/drizzle/methods/ItemPackTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { DbClient } from '../../db/client';
import { eq, and } from 'drizzle-orm';
import {
type InsertItemPackTemplate,
itemPackTemplate as ItemPackTemplateTable,
} from '../../db/schema';

export class ItemPackTemplate {
async create(data: InsertItemPackTemplate) {
try {
const item = await DbClient.instance
.insert(ItemPackTemplateTable)
.values(data)
.returning()
.get();

return item;
} catch (error) {
throw new Error(`Failed to create item: ${error.message}`);
}
}

async findByItemIdAndPackTemplateId({
itemId,
packTemplateId,
}: {
itemId: string;
packTemplateId: string;
}) {
const itemFilter = eq(ItemPackTemplateTable.itemId, itemId);
const packFilter = eq(ItemPackTemplateTable.packTemplateId, packTemplateId);

const filter = and(itemFilter, packFilter);

return await DbClient.instance.query.itemPackTemplate.findFirst({
where: filter,
});
}
}
Loading
Loading