Skip to content

Commit

Permalink
Merge pull request #1351 from andrew-bierman/import-packTemplate
Browse files Browse the repository at this point in the history
Create public end Point to import pack template
  • Loading branch information
taronaleksanian authored Dec 15, 2024
2 parents 3045bef + 6f2c05b commit c8ea5b5
Show file tree
Hide file tree
Showing 21 changed files with 499 additions and 339 deletions.
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

0 comments on commit c8ea5b5

Please sign in to comment.