From de3ca13138847b8e438fe23a63f4e7b190b27ae4 Mon Sep 17 00:00:00 2001 From: Tadjaur Date: Tue, 10 Dec 2024 01:42:10 +0100 Subject: [PATCH 1/4] Create public endPoint to import pack template --- .../DownloadMapBtn/DownloadMapBtn.web.tsx | 7 -- .../packTemplateRoutesValidator.ts | 27 +++++- server/package.json | 2 + .../packTemplates/addPackTemplate.ts | 43 ++++++++++ .../packTemplates/getPackTemplate.ts | 3 +- server/src/controllers/packTemplates/index.ts | 3 +- server/src/db/schema.ts | 20 +++-- .../src/drizzle/methods/ItemPackTemplate.ts | 39 +++++++++ server/src/drizzle/methods/PackTemplate.ts | 67 ++++++++++++--- server/src/routes/trpcRouter.ts | 4 + .../itemPackTemplate/addItemPackTemplate.ts | 28 ++++++ .../itemPackTemplate.service.ts | 1 + .../packTemplate/addPackTemplateService.ts | 53 ++++++++++++ .../createPackFromTemplateService.ts | 5 +- .../packTemplate/getPackTemplateService.ts | 6 +- .../packTemplate/packTemplate.service.ts | 1 + server/src/tests/routes/packTemplate.spec.ts | 15 +++- yarn.lock | 86 ++++++++++++++++++- 18 files changed, 376 insertions(+), 34 deletions(-) delete mode 100644 packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.web.tsx create mode 100644 server/src/controllers/packTemplates/addPackTemplate.ts create mode 100644 server/src/drizzle/methods/ItemPackTemplate.ts create mode 100644 server/src/services/itemPackTemplate/addItemPackTemplate.ts create mode 100644 server/src/services/itemPackTemplate/itemPackTemplate.service.ts create mode 100644 server/src/services/packTemplate/addPackTemplateService.ts diff --git a/packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.web.tsx b/packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.web.tsx deleted file mode 100644 index 6e6a23f51..000000000 --- a/packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.web.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React, { type FC } from 'react'; -import { View } from 'react-native'; - -export const DownloadMapBtn: FC = () => { - // Returns empty component on the on the web implementation. - return ; -}; diff --git a/packages/validations/src/validations/packTemplateRoutesValidator.ts b/packages/validations/src/validations/packTemplateRoutesValidator.ts index a98162019..3a977d1b6 100644 --- a/packages/validations/src/validations/packTemplateRoutesValidator.ts +++ b/packages/validations/src/validations/packTemplateRoutesValidator.ts @@ -13,11 +13,32 @@ 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(), + }), +] as const); 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(), + itemPackTemplates: z.array( + z.object({ + itemId: z.string(), + quantity: z.number(), + }), + ), +}); + +export const addPackTemplates = z.array(addPackTemplate); diff --git a/server/package.json b/server/package.json index 5c36092a6..f89f15b21 100644 --- a/server/package.json +++ b/server/package.json @@ -3,6 +3,7 @@ "version": "1.1.1", "description": "", "main": "index.js", + "type": "module", "private": true, "scripts": { "build": "NODE_OPTIONS=--max_old_space_size=4096 tsc", @@ -143,6 +144,7 @@ "expect.js": "^0.3.1", "jest": "^29.7.0", "migrate-mongo": "^10.0.0", + "miniflare": "^3.20241205.0", "mocha": "^10.2.0", "openapi-generator": "^0.1.39", "prettier": "^3.2.5", diff --git a/server/src/controllers/packTemplates/addPackTemplate.ts b/server/src/controllers/packTemplates/addPackTemplate.ts new file mode 100644 index 000000000..5c28299b7 --- /dev/null +++ b/server/src/controllers/packTemplates/addPackTemplate.ts @@ -0,0 +1,43 @@ +import { addPackTemplateService } from '../../services/packTemplate/packTemplate.service'; +import * as validator from '@packrat/validations'; +import { publicProcedure } from '../../trpc'; +import { th } from '@faker-js/faker'; + +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 { name, description, type, itemPackTemplates } = array[idx]; + try { + const template = await addPackTemplateService({ + name, + description, + type, + itemPackTemplates, + }); + packTemplates.push(template); + } catch (error) { + console.log(error); + throw error; + } + } + }); +} + +export function addPackTemplateRoute() { + return publicProcedure + .input(validator.addPackTemplate) + .mutation(async (opts) => { + const { name, description, type, itemPackTemplates } = opts.input; + const packTemplate = await addPackTemplateService({ + name, + description, + type, + itemPackTemplates + }); + return packTemplate; + }); +} diff --git a/server/src/controllers/packTemplates/getPackTemplate.ts b/server/src/controllers/packTemplates/getPackTemplate.ts index 41713c24a..03942e002 100644 --- a/server/src/controllers/packTemplates/getPackTemplate.ts +++ b/server/src/controllers/packTemplates/getPackTemplate.ts @@ -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); }); } diff --git a/server/src/controllers/packTemplates/index.ts b/server/src/controllers/packTemplates/index.ts index e83211275..31d9ba1a1 100644 --- a/server/src/controllers/packTemplates/index.ts +++ b/server/src/controllers/packTemplates/index.ts @@ -1,3 +1,4 @@ +export * from './addPackTemplate'; export * from './getPackTemplates'; -export * from './getPackTemplate' +export * from './getPackTemplate'; export * from './createPackFromTemplate'; diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index 7ce1a31b8..fc34ccde2 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -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' }), @@ -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], }), }), @@ -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', { @@ -611,8 +612,17 @@ export const insertTemplateSchema = createInsertSchema(template); export const selectTemplateSchema = createSelectSchema(template); export type PackTemplate = InferSelectModel; +export type InsertPackTemplate = InferInsertModel; +export const insertPackTemplateSchema = createInsertSchema(packTemplate); export const selectPackTemplateSchema = createSelectSchema(packTemplate); +export type ItemPackTemplate = InferSelectModel; +export type InsertItemPackTemplate = InferInsertModel; +export const insertItemPackTemplateSchema = + createInsertSchema(itemPackTemplate); +export const selectItemPackTemplateSchema = + createSelectSchema(itemPackTemplate); + export type Pack = InferSelectModel; export type InsertPack = InferInsertModel; export const insertPackSchema = createInsertSchema(pack); diff --git a/server/src/drizzle/methods/ItemPackTemplate.ts b/server/src/drizzle/methods/ItemPackTemplate.ts new file mode 100644 index 000000000..5dfef9e9f --- /dev/null +++ b/server/src/drizzle/methods/ItemPackTemplate.ts @@ -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, + }); + } +} diff --git a/server/src/drizzle/methods/PackTemplate.ts b/server/src/drizzle/methods/PackTemplate.ts index 1413bd8b8..d9ac6b475 100644 --- a/server/src/drizzle/methods/PackTemplate.ts +++ b/server/src/drizzle/methods/PackTemplate.ts @@ -1,12 +1,17 @@ import { asc, count, desc, eq, like, sql } from 'drizzle-orm'; import { DbClient } from '../../db/client'; import { convertWeight, type WeightUnit } from 'src/utils/convertWeight'; -import { packTemplate, item, itemPackTemplates } from 'src/db/schema'; +import { + packTemplate, + item, + itemPackTemplate, + InsertPackTemplate, +} from 'src/db/schema'; import { PaginationParams } from 'src/helpers/pagination'; -export type Filter = { +export interface Filter { searchQuery?: string; -}; +} export type ORDER_BY = 'Lightest' | 'Heaviest'; @@ -27,17 +32,17 @@ export class PackTemplate { type: packTemplate.type, description: packTemplate.description, total_weight: - sql`SUM(${item.weight} * ${itemPackTemplates.quantity})`.as( + sql`SUM(${item.weight} * ${itemPackTemplate.quantity})`.as( 'total_weight', ), - quantity: sql`SUM(${itemPackTemplates.quantity})`, + quantity: sql`SUM(${itemPackTemplate.quantity})`, }) .from(packTemplate) .leftJoin( - itemPackTemplates, - eq(packTemplate.id, itemPackTemplates.packTemplateId), + itemPackTemplate, + eq(packTemplate.id, itemPackTemplate.packTemplateId), ) - .leftJoin(item, eq(itemPackTemplates.itemId, item.id)) + .leftJoin(item, eq(itemPackTemplate.itemId, item.id)) .groupBy(packTemplate.id); if (filter?.searchQuery) { @@ -68,15 +73,54 @@ export class PackTemplate { }; } - async findPackTemplate(id: string) { + /** + * Creates a pack template. + * @param data The date used to create a pack template. + */ + async create(data: InsertPackTemplate) { + try { + const createdPackTemplate = await DbClient.instance + .insert(packTemplate) + .values(data) + .returning() + .get(); + return createdPackTemplate; + } catch (error) { + throw new Error(`Failed to create a pack: ${error.message}`); + } + } + + /** + * Finds a pack template by its ID or name. + * @param params The parameters to search for a pack template. + * @param params.id The ID of the pack template to search for. + * @param params.name The name of the pack template to search for. + * @returns + */ + async findPackTemplate( + params: { id: string; name?: undefined } | { name: string; id?: undefined }, + ) { + const { id, name } = params; + let filter; + if (id) { + filter = eq(packTemplate.id, id); + } else if (name) { + filter = eq(packTemplate.name, name); + } else { + throw new Error('Either id or name must be provided'); + } const packTemplateResult = await DbClient.instance.query.packTemplate.findFirst({ - where: eq(packTemplate.id, id), + where: filter, with: { itemPackTemplates: { with: { item: { with: { category: {} } } } }, }, }); + if (!packTemplateResult) { + return packTemplateResult; + } + const items = packTemplateResult.itemPackTemplates.map( (itemPackTemplate) => ({ ...itemPackTemplate.item, @@ -91,13 +135,14 @@ export class PackTemplate { ); return sum + weightInGrams * item.quantity; }, 0); + const quantity = items.reduce((sum, item) => sum + item.quantity, 0); delete packTemplateResult.itemPackTemplates; return { ...packTemplateResult, - items, + itemsPackTemplate: items, total_weight, quantity, }; diff --git a/server/src/routes/trpcRouter.ts b/server/src/routes/trpcRouter.ts index 97d36e693..972100773 100644 --- a/server/src/routes/trpcRouter.ts +++ b/server/src/routes/trpcRouter.ts @@ -106,6 +106,8 @@ import { getPackTemplatesRoute, getPackTemplateRoute, createPackFromTemplateRoute, + addPackTemplateRoute, + importPackTemplatesRoute, } from '../controllers/packTemplates'; import { router as trpcRouter } from '../trpc'; @@ -151,6 +153,8 @@ export const appRouter = trpcRouter({ getPackTemplates: getPackTemplatesRoute(), getPackTemplate: getPackTemplateRoute(), createPackFromTemplate: createPackFromTemplateRoute(), + addPackTemplate: addPackTemplateRoute(), + addPackTemplates: importPackTemplatesRoute(), getTemplates: getTemplatesRoute(), getTemplateById: getTemplateByIdRoute(), addTemplate: addTemplateRoute(), diff --git a/server/src/services/itemPackTemplate/addItemPackTemplate.ts b/server/src/services/itemPackTemplate/addItemPackTemplate.ts new file mode 100644 index 000000000..fefd556c4 --- /dev/null +++ b/server/src/services/itemPackTemplate/addItemPackTemplate.ts @@ -0,0 +1,28 @@ +import { ItemPackTemplate } from '../../drizzle/methods/ItemPackTemplate'; + +/** + * Adds a item to the pack template service. + * @param {string} packTemplateId - The ID of the pack template. + * @param {string} itemId - The ID reference of the item. + * @param {number} quantity - The ID of the owner. + * @return {Promise} - A promise that resolves to the added item. + */ +export const addItemPackTemplate = async (params: { + packTemplateId: string; + itemId: string; + quantity: number; +}) => { + const { packTemplateId, itemId, quantity } = params; + const itemPackTemplate = new ItemPackTemplate(); + const item = await itemPackTemplate.findByItemIdAndPackTemplateId({ + packTemplateId, + itemId, + }); + + if (item) { + throw new Error( + 'A template item with the same item and pack references already exists', + ); + } + await itemPackTemplate.create({ itemId, packTemplateId, quantity }); +}; diff --git a/server/src/services/itemPackTemplate/itemPackTemplate.service.ts b/server/src/services/itemPackTemplate/itemPackTemplate.service.ts new file mode 100644 index 000000000..0c6fa2785 --- /dev/null +++ b/server/src/services/itemPackTemplate/itemPackTemplate.service.ts @@ -0,0 +1 @@ +export * from './addItemPackTemplate'; diff --git a/server/src/services/packTemplate/addPackTemplateService.ts b/server/src/services/packTemplate/addPackTemplateService.ts new file mode 100644 index 000000000..4ced9cb47 --- /dev/null +++ b/server/src/services/packTemplate/addPackTemplateService.ts @@ -0,0 +1,53 @@ +import { PackTemplate as PackTemplateClass } from '../../drizzle/methods/PackTemplate'; +import * as ItemPackTemplateService from '../itemPackTemplate/itemPackTemplate.service'; +import { PackTemplate } from 'src/db/schema'; + +/** + * Adds a new pack template service. + * @param {Object} packTemplateData - The data for the new pack template. + * @param {string} packTemplateData.name - The name of the pack. + * @param {string} packTemplateData.description - The description of the pack. + * @param {string} packTemplateData.type - Whether the pack is public or not. + * @return {Object} An object containing the created pack. + */ +export const addPackTemplateService = async (packTemplateData: { + name: string; + description: string; + type: string; + itemPackTemplates: Array<{ + itemId: string; + quantity: number; + }>; +}): Promise => { + const { name, description, type } = packTemplateData; + console.log({ packTemplateData }); + const packTemplateClass = new PackTemplateClass(); + let existingPack: PackTemplate | null = + await packTemplateClass.findPackTemplate({ name }); + + if (!existingPack) { + existingPack = await packTemplateClass.create({ + name, + description, + type, + }); + } else { + console.log( + 'Pack template already exists. Skipping creation and proceeding with update of items', + ); + } + + for (const itemPackTemplate of packTemplateData.itemPackTemplates) { + try { + await ItemPackTemplateService.addItemPackTemplate({ + itemId: itemPackTemplate.itemId, + quantity: itemPackTemplate.quantity, + packTemplateId: existingPack.id, + }); + } catch (error) { + console.log(error); + } + } + + return existingPack; +}; diff --git a/server/src/services/packTemplate/createPackFromTemplateService.ts b/server/src/services/packTemplate/createPackFromTemplateService.ts index a7828c5dc..df93a0312 100644 --- a/server/src/services/packTemplate/createPackFromTemplateService.ts +++ b/server/src/services/packTemplate/createPackFromTemplateService.ts @@ -11,8 +11,9 @@ export const createPackFromTemplateService = async ( ) => { const packTemplateRepository = new PackTemplateRepository(); - const packTemplate = - await packTemplateRepository.findPackTemplate(packTemplateId); + const packTemplate = await packTemplateRepository.findPackTemplate({ + id: packTemplateId, + }); // TODO - creating pack and adding items to it should ideally be transactional const createdPack = await addPackService( diff --git a/server/src/services/packTemplate/getPackTemplateService.ts b/server/src/services/packTemplate/getPackTemplateService.ts index e03ea3a9e..cb86e2846 100644 --- a/server/src/services/packTemplate/getPackTemplateService.ts +++ b/server/src/services/packTemplate/getPackTemplateService.ts @@ -1,5 +1,7 @@ import { PackTemplate } from 'src/drizzle/methods/PackTemplate'; -export async function getPackTemplateService(packTemplateId: string) { - return await new PackTemplate().findPackTemplate(packTemplateId); +export async function getPackTemplateService( + params: { id: string; name?: undefined } | { name: string; id?: undefined }, +) { + return await new PackTemplate().findPackTemplate(params); } diff --git a/server/src/services/packTemplate/packTemplate.service.ts b/server/src/services/packTemplate/packTemplate.service.ts index 40fd201b4..191893919 100644 --- a/server/src/services/packTemplate/packTemplate.service.ts +++ b/server/src/services/packTemplate/packTemplate.service.ts @@ -1,3 +1,4 @@ +export * from './addPackTemplateService'; export * from './getPackTemplatesService'; export * from './getPackTemplateService'; export * from './createPackFromTemplateService'; diff --git a/server/src/tests/routes/packTemplate.spec.ts b/server/src/tests/routes/packTemplate.spec.ts index 59d02867a..5af0be490 100644 --- a/server/src/tests/routes/packTemplate.spec.ts +++ b/server/src/tests/routes/packTemplate.spec.ts @@ -7,7 +7,7 @@ import type { trpcCaller } from '../testHelpers'; import { type PackTemplate, type Item, - itemPackTemplates as itemPackTemplatesTable, + itemPackTemplate as itemPackTemplatesTable, packTemplate as packTemplateTable, } from 'src/db/schema'; import { DbClient } from '../../db/client'; @@ -117,6 +117,19 @@ describe('Pack template routes', () => { }); }); + describe('addPackTemplate', () => { + it('should add pack template', async () => { + const packTemplate = await caller.addPackTemplate({ + name: 'test', + description: 'pack template description', + type: 'pack', + }); + expect(packTemplate).toMatchObject([ + { ...packTemplate, items: packTemplateItems }, + ]); + }); + }); + describe('getPackTemplate', () => { it('should get a pack template', async () => { const packTemplateResult = await caller.getPackTemplate({ diff --git a/yarn.lock b/yarn.lock index 9c0a2e1ca..fa86d6778 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2345,6 +2345,13 @@ __metadata: languageName: node linkType: hard +"@cloudflare/workerd-darwin-64@npm:1.20241205.0": + version: 1.20241205.0 + resolution: "@cloudflare/workerd-darwin-64@npm:1.20241205.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@cloudflare/workerd-darwin-arm64@npm:1.20240405.0": version: 1.20240405.0 resolution: "@cloudflare/workerd-darwin-arm64@npm:1.20240405.0" @@ -2359,6 +2366,13 @@ __metadata: languageName: node linkType: hard +"@cloudflare/workerd-darwin-arm64@npm:1.20241205.0": + version: 1.20241205.0 + resolution: "@cloudflare/workerd-darwin-arm64@npm:1.20241205.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@cloudflare/workerd-linux-64@npm:1.20240405.0": version: 1.20240405.0 resolution: "@cloudflare/workerd-linux-64@npm:1.20240405.0" @@ -2373,6 +2387,13 @@ __metadata: languageName: node linkType: hard +"@cloudflare/workerd-linux-64@npm:1.20241205.0": + version: 1.20241205.0 + resolution: "@cloudflare/workerd-linux-64@npm:1.20241205.0" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@cloudflare/workerd-linux-arm64@npm:1.20240405.0": version: 1.20240405.0 resolution: "@cloudflare/workerd-linux-arm64@npm:1.20240405.0" @@ -2387,6 +2408,13 @@ __metadata: languageName: node linkType: hard +"@cloudflare/workerd-linux-arm64@npm:1.20241205.0": + version: 1.20241205.0 + resolution: "@cloudflare/workerd-linux-arm64@npm:1.20241205.0" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@cloudflare/workerd-windows-64@npm:1.20240405.0": version: 1.20240405.0 resolution: "@cloudflare/workerd-windows-64@npm:1.20240405.0" @@ -2401,6 +2429,13 @@ __metadata: languageName: node linkType: hard +"@cloudflare/workerd-windows-64@npm:1.20241205.0": + version: 1.20241205.0 + resolution: "@cloudflare/workerd-windows-64@npm:1.20241205.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@cloudflare/workers-shared@npm:0.5.3": version: 0.5.3 resolution: "@cloudflare/workers-shared@npm:0.5.3" @@ -28779,6 +28814,28 @@ __metadata: languageName: node linkType: hard +"miniflare@npm:^3.20241205.0": + version: 3.20241205.0 + resolution: "miniflare@npm:3.20241205.0" + dependencies: + "@cspotcode/source-map-support": "npm:0.8.1" + acorn: "npm:^8.8.0" + acorn-walk: "npm:^8.2.0" + capnp-ts: "npm:^0.7.0" + exit-hook: "npm:^2.2.1" + glob-to-regexp: "npm:^0.4.1" + stoppable: "npm:^1.1.0" + undici: "npm:^5.28.4" + workerd: "npm:1.20241205.0" + ws: "npm:^8.18.0" + youch: "npm:^3.2.2" + zod: "npm:^3.22.3" + bin: + miniflare: bootstrap.js + checksum: 10/0efbb456890f0293af129b4850c6e420530384d674ed02e95b9195fdabba270b6016bd44cc52b92a4d9be037b0e522949d6ab463c17dfadc86014fefcba461b7 + languageName: node + linkType: hard + "minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" @@ -34924,6 +34981,7 @@ __metadata: jsonwebtoken: "npm:^9.0.1" langchain: "npm:^0.0.203" migrate-mongo: "npm:^10.0.0" + miniflare: "npm:^3.20241205.0" mocha: "npm:^10.2.0" mock-aws-s3: "npm:^4.0.2" mongoose: "npm:^7.4.0" @@ -39271,6 +39329,32 @@ __metadata: languageName: node linkType: hard +"workerd@npm:1.20241205.0": + version: 1.20241205.0 + resolution: "workerd@npm:1.20241205.0" + dependencies: + "@cloudflare/workerd-darwin-64": "npm:1.20241205.0" + "@cloudflare/workerd-darwin-arm64": "npm:1.20241205.0" + "@cloudflare/workerd-linux-64": "npm:1.20241205.0" + "@cloudflare/workerd-linux-arm64": "npm:1.20241205.0" + "@cloudflare/workerd-windows-64": "npm:1.20241205.0" + dependenciesMeta: + "@cloudflare/workerd-darwin-64": + optional: true + "@cloudflare/workerd-darwin-arm64": + optional: true + "@cloudflare/workerd-linux-64": + optional: true + "@cloudflare/workerd-linux-arm64": + optional: true + "@cloudflare/workerd-windows-64": + optional: true + bin: + workerd: bin/workerd + checksum: 10/c66c79f290a247f86f28182218f4d1c54736b6a954827a447299fd7b38a4d9483b3ee10c936335a336594001c174e62d578f7a95d2044f56b56d1162eb9aa719 + languageName: node + linkType: hard + "workerpool@npm:^6.5.1": version: 6.5.1 resolution: "workerpool@npm:6.5.1" @@ -39446,7 +39530,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.11.0, ws@npm:^8.12.1, ws@npm:^8.13.0, ws@npm:^8.17.1": +"ws@npm:^8.11.0, ws@npm:^8.12.1, ws@npm:^8.13.0, ws@npm:^8.17.1, ws@npm:^8.18.0": version: 8.18.0 resolution: "ws@npm:8.18.0" peerDependencies: From 0ce2ea130de5b4ed4b6bea1699ec7c6fa416423f Mon Sep 17 00:00:00 2001 From: Tadjaur Date: Tue, 10 Dec 2024 01:44:46 +0100 Subject: [PATCH 2/4] revert the minifar installation --- .../DownloadMapBtn/DownloadMapBtn.web.tsx | 7 ++ server/package.json | 2 - yarn.lock | 86 +------------------ 3 files changed, 8 insertions(+), 87 deletions(-) create mode 100644 packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.web.tsx diff --git a/packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.web.tsx b/packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.web.tsx new file mode 100644 index 000000000..6e6a23f51 --- /dev/null +++ b/packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.web.tsx @@ -0,0 +1,7 @@ +import React, { type FC } from 'react'; +import { View } from 'react-native'; + +export const DownloadMapBtn: FC = () => { + // Returns empty component on the on the web implementation. + return ; +}; diff --git a/server/package.json b/server/package.json index f89f15b21..5c36092a6 100644 --- a/server/package.json +++ b/server/package.json @@ -3,7 +3,6 @@ "version": "1.1.1", "description": "", "main": "index.js", - "type": "module", "private": true, "scripts": { "build": "NODE_OPTIONS=--max_old_space_size=4096 tsc", @@ -144,7 +143,6 @@ "expect.js": "^0.3.1", "jest": "^29.7.0", "migrate-mongo": "^10.0.0", - "miniflare": "^3.20241205.0", "mocha": "^10.2.0", "openapi-generator": "^0.1.39", "prettier": "^3.2.5", diff --git a/yarn.lock b/yarn.lock index fa86d6778..9c0a2e1ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2345,13 +2345,6 @@ __metadata: languageName: node linkType: hard -"@cloudflare/workerd-darwin-64@npm:1.20241205.0": - version: 1.20241205.0 - resolution: "@cloudflare/workerd-darwin-64@npm:1.20241205.0" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@cloudflare/workerd-darwin-arm64@npm:1.20240405.0": version: 1.20240405.0 resolution: "@cloudflare/workerd-darwin-arm64@npm:1.20240405.0" @@ -2366,13 +2359,6 @@ __metadata: languageName: node linkType: hard -"@cloudflare/workerd-darwin-arm64@npm:1.20241205.0": - version: 1.20241205.0 - resolution: "@cloudflare/workerd-darwin-arm64@npm:1.20241205.0" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@cloudflare/workerd-linux-64@npm:1.20240405.0": version: 1.20240405.0 resolution: "@cloudflare/workerd-linux-64@npm:1.20240405.0" @@ -2387,13 +2373,6 @@ __metadata: languageName: node linkType: hard -"@cloudflare/workerd-linux-64@npm:1.20241205.0": - version: 1.20241205.0 - resolution: "@cloudflare/workerd-linux-64@npm:1.20241205.0" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@cloudflare/workerd-linux-arm64@npm:1.20240405.0": version: 1.20240405.0 resolution: "@cloudflare/workerd-linux-arm64@npm:1.20240405.0" @@ -2408,13 +2387,6 @@ __metadata: languageName: node linkType: hard -"@cloudflare/workerd-linux-arm64@npm:1.20241205.0": - version: 1.20241205.0 - resolution: "@cloudflare/workerd-linux-arm64@npm:1.20241205.0" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@cloudflare/workerd-windows-64@npm:1.20240405.0": version: 1.20240405.0 resolution: "@cloudflare/workerd-windows-64@npm:1.20240405.0" @@ -2429,13 +2401,6 @@ __metadata: languageName: node linkType: hard -"@cloudflare/workerd-windows-64@npm:1.20241205.0": - version: 1.20241205.0 - resolution: "@cloudflare/workerd-windows-64@npm:1.20241205.0" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@cloudflare/workers-shared@npm:0.5.3": version: 0.5.3 resolution: "@cloudflare/workers-shared@npm:0.5.3" @@ -28814,28 +28779,6 @@ __metadata: languageName: node linkType: hard -"miniflare@npm:^3.20241205.0": - version: 3.20241205.0 - resolution: "miniflare@npm:3.20241205.0" - dependencies: - "@cspotcode/source-map-support": "npm:0.8.1" - acorn: "npm:^8.8.0" - acorn-walk: "npm:^8.2.0" - capnp-ts: "npm:^0.7.0" - exit-hook: "npm:^2.2.1" - glob-to-regexp: "npm:^0.4.1" - stoppable: "npm:^1.1.0" - undici: "npm:^5.28.4" - workerd: "npm:1.20241205.0" - ws: "npm:^8.18.0" - youch: "npm:^3.2.2" - zod: "npm:^3.22.3" - bin: - miniflare: bootstrap.js - checksum: 10/0efbb456890f0293af129b4850c6e420530384d674ed02e95b9195fdabba270b6016bd44cc52b92a4d9be037b0e522949d6ab463c17dfadc86014fefcba461b7 - languageName: node - linkType: hard - "minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" @@ -34981,7 +34924,6 @@ __metadata: jsonwebtoken: "npm:^9.0.1" langchain: "npm:^0.0.203" migrate-mongo: "npm:^10.0.0" - miniflare: "npm:^3.20241205.0" mocha: "npm:^10.2.0" mock-aws-s3: "npm:^4.0.2" mongoose: "npm:^7.4.0" @@ -39329,32 +39271,6 @@ __metadata: languageName: node linkType: hard -"workerd@npm:1.20241205.0": - version: 1.20241205.0 - resolution: "workerd@npm:1.20241205.0" - dependencies: - "@cloudflare/workerd-darwin-64": "npm:1.20241205.0" - "@cloudflare/workerd-darwin-arm64": "npm:1.20241205.0" - "@cloudflare/workerd-linux-64": "npm:1.20241205.0" - "@cloudflare/workerd-linux-arm64": "npm:1.20241205.0" - "@cloudflare/workerd-windows-64": "npm:1.20241205.0" - dependenciesMeta: - "@cloudflare/workerd-darwin-64": - optional: true - "@cloudflare/workerd-darwin-arm64": - optional: true - "@cloudflare/workerd-linux-64": - optional: true - "@cloudflare/workerd-linux-arm64": - optional: true - "@cloudflare/workerd-windows-64": - optional: true - bin: - workerd: bin/workerd - checksum: 10/c66c79f290a247f86f28182218f4d1c54736b6a954827a447299fd7b38a4d9483b3ee10c936335a336594001c174e62d578f7a95d2044f56b56d1162eb9aa719 - languageName: node - linkType: hard - "workerpool@npm:^6.5.1": version: 6.5.1 resolution: "workerpool@npm:6.5.1" @@ -39530,7 +39446,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.11.0, ws@npm:^8.12.1, ws@npm:^8.13.0, ws@npm:^8.17.1, ws@npm:^8.18.0": +"ws@npm:^8.11.0, ws@npm:^8.12.1, ws@npm:^8.13.0, ws@npm:^8.17.1": version: 8.18.0 resolution: "ws@npm:8.18.0" peerDependencies: From 8771cd41b2af5e15d4a2c116914b39edad3c991a Mon Sep 17 00:00:00 2001 From: Tadjaur Date: Wed, 11 Dec 2024 12:20:52 +0100 Subject: [PATCH 3/4] Update the input data --- .../src/validations/itemRoutesValidator.ts | 16 +++ .../packTemplateRoutesValidator.ts | 4 +- .../src/controllers/item/importItemsGlobal.ts | 4 +- .../packTemplates/addPackTemplate.ts | 27 ++-- .../packTemplates/getPackTemplate.ts | 4 +- server/src/controllers/user/getUsers.ts | 4 +- server/src/drizzle/methods/Item.ts | 17 +++ server/src/drizzle/methods/PackTemplate.ts | 1 + .../src/services/item/addItemGlobalService.ts | 98 ++++---------- server/src/services/item/addItemService.ts | 121 +++++++++++++++++- .../packTemplate/addPackTemplateService.ts | 50 +++++--- 11 files changed, 226 insertions(+), 120 deletions(-) diff --git a/packages/validations/src/validations/itemRoutesValidator.ts b/packages/validations/src/validations/itemRoutesValidator.ts index 890257491..0e0ba3c2a 100644 --- a/packages/validations/src/validations/itemRoutesValidator.ts +++ b/packages/validations/src/validations/itemRoutesValidator.ts @@ -70,6 +70,22 @@ export const addItemGlobal = z.object({ ownerId: z.string(), }); +export const addNewItemGlobal = z.object({ + name: z.string(), + weight: z.number(), + unit: z.string(), + category: z.enum(['Food', 'Water', 'Essentials']), + sku: z.string(), + productUrl: z.string(), + description: z.string(), + productDetails: z.record( + z.string(), + z.union([z.string(), z.number(), z.boolean()]), + ), + seller: z.string(), + imageUrls: z.string(), +}); + export const importItemsGlobal = z.object({ content: z.string(), ownerId: z.string(), diff --git a/packages/validations/src/validations/packTemplateRoutesValidator.ts b/packages/validations/src/validations/packTemplateRoutesValidator.ts index 3a977d1b6..2458c9db5 100644 --- a/packages/validations/src/validations/packTemplateRoutesValidator.ts +++ b/packages/validations/src/validations/packTemplateRoutesValidator.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { addNewItemGlobal } from './itemRoutesValidator'; export const getPackTemplates = z.object({ filter: z @@ -33,9 +34,10 @@ export const addPackTemplate = z.object({ name: z.string(), description: z.string(), type: z.string(), + itemsOwnerId: z.string(), itemPackTemplates: z.array( z.object({ - itemId: z.string(), + item: addNewItemGlobal, quantity: z.number(), }), ), diff --git a/server/src/controllers/item/importItemsGlobal.ts b/server/src/controllers/item/importItemsGlobal.ts index e29dc511d..fc15e1de9 100644 --- a/server/src/controllers/item/importItemsGlobal.ts +++ b/server/src/controllers/item/importItemsGlobal.ts @@ -117,6 +117,8 @@ export function importItemsGlobalRoute() { let idx = 0; await addItemGlobalServiceBatch( results.data, + true, + opts.ctx.executionCtx, (item) => { const productDetailsStr = `${item.techs}` .replace(/'([^']*)'\s*:/g, '"$1":') // Replace single quotes keys with double quotes. @@ -153,8 +155,6 @@ export function importItemsGlobalRoute() { productDetails: JSON.parse(productDetailsStr), }; }, - false, - opts.ctx.executionCtx, ); return resolve('items'); } catch (error) { diff --git a/server/src/controllers/packTemplates/addPackTemplate.ts b/server/src/controllers/packTemplates/addPackTemplate.ts index 5c28299b7..310551123 100644 --- a/server/src/controllers/packTemplates/addPackTemplate.ts +++ b/server/src/controllers/packTemplates/addPackTemplate.ts @@ -1,7 +1,6 @@ import { addPackTemplateService } from '../../services/packTemplate/packTemplate.service'; import * as validator from '@packrat/validations'; import { publicProcedure } from '../../trpc'; -import { th } from '@faker-js/faker'; export function importPackTemplatesRoute() { return publicProcedure @@ -10,20 +9,19 @@ export function importPackTemplatesRoute() { const array = opts.input; const packTemplates = []; for (let idx = 0; idx < array.length; idx++) { - const { name, description, type, itemPackTemplates } = array[idx]; + const input = array[idx]; try { - const template = await addPackTemplateService({ - name, - description, - type, - itemPackTemplates, - }); - packTemplates.push(template); + const packTemplate = await addPackTemplateService( + input, + opts.ctx.executionCtx, + ); + packTemplates.push(packTemplate); } catch (error) { console.log(error); throw error; } } + return packTemplates; }); } @@ -31,13 +29,10 @@ export function addPackTemplateRoute() { return publicProcedure .input(validator.addPackTemplate) .mutation(async (opts) => { - const { name, description, type, itemPackTemplates } = opts.input; - const packTemplate = await addPackTemplateService({ - name, - description, - type, - itemPackTemplates - }); + const packTemplate = await addPackTemplateService( + opts.input, + opts.ctx.executionCtx, + ); return packTemplate; }); } diff --git a/server/src/controllers/packTemplates/getPackTemplate.ts b/server/src/controllers/packTemplates/getPackTemplate.ts index 03942e002..69eeda2e2 100644 --- a/server/src/controllers/packTemplates/getPackTemplate.ts +++ b/server/src/controllers/packTemplates/getPackTemplate.ts @@ -1,9 +1,9 @@ import * as validator from '@packrat/validations'; -import { protectedProcedure } from 'src/trpc'; +import { publicProcedure } from 'src/trpc'; import { getPackTemplateService } from 'src/services/packTemplate/packTemplate.service'; export function getPackTemplateRoute() { - return protectedProcedure + return publicProcedure .input(validator.getPackTemplate) .query(async ({ input }) => { const param = input.id ? { id: input.id } : { name: input.name }; diff --git a/server/src/controllers/user/getUsers.ts b/server/src/controllers/user/getUsers.ts index 6721b0655..ece62d01e 100644 --- a/server/src/controllers/user/getUsers.ts +++ b/server/src/controllers/user/getUsers.ts @@ -1,4 +1,4 @@ -import { protectedProcedure } from '../../trpc'; +import { protectedProcedure, publicProcedure } from '../../trpc'; import { responseHandler } from '../../helpers/responseHandler'; import { User } from '../../drizzle/methods/User'; @@ -13,7 +13,7 @@ export const getUsers = async (c) => { }; export function getUsersRoute() { - return protectedProcedure.query(async (opts) => { + return publicProcedure.query(async (opts) => { const userClass = new User(); const users = await userClass.findMany(); return users; diff --git a/server/src/drizzle/methods/Item.ts b/server/src/drizzle/methods/Item.ts index f2e5bbc18..7dee75417 100644 --- a/server/src/drizzle/methods/Item.ts +++ b/server/src/drizzle/methods/Item.ts @@ -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'; @@ -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); diff --git a/server/src/drizzle/methods/PackTemplate.ts b/server/src/drizzle/methods/PackTemplate.ts index d9ac6b475..8a7679469 100644 --- a/server/src/drizzle/methods/PackTemplate.ts +++ b/server/src/drizzle/methods/PackTemplate.ts @@ -117,6 +117,7 @@ export class PackTemplate { }, }); + if (!packTemplateResult) { return packTemplateResult; } diff --git a/server/src/services/item/addItemGlobalService.ts b/server/src/services/item/addItemGlobalService.ts index 01b8af03c..567c8390b 100644 --- a/server/src/services/item/addItemGlobalService.ts +++ b/server/src/services/item/addItemGlobalService.ts @@ -12,6 +12,7 @@ import { convertWeight, SMALLEST_WEIGHT_UNIT } from 'src/utils/convertWeight'; import { DbClient } from 'src/db/client'; import { itemImage as itemImageTable } from '../../db/schema'; import { summarizeItem } from 'src/utils/item'; +import { addNewItemService, bulkAddNewItemsService } from './addItemService'; // import { prisma } from '../../prisma'; interface AddItemGlobalServiceParams { @@ -117,87 +118,36 @@ export const addItemGlobalService = async ( */ export const addItemGlobalServiceBatch = async ( rawItems: T[], - transform: (rawItem: T) => AddItemGlobalServiceParams, continueOnError = false, executionCtx: ExecutionContext, + transform: (rawItem: T) => AddItemGlobalServiceParams, ) => { - const errors = []; + const errors: Error[] = []; - const createdItemsInOrder: Array<[itemIndex: number, item: Item]> = []; - for (let idx = 0; idx < rawItems.length; idx++) { - const item = transform(rawItems[idx]); - let category: InsertItemCategory | null; - if (!categories.includes(item.type)) { - const error = new Error( - `[${item.sku}#${item.name}]: Category must be one of: ${categories.join(', ')}`, - ); - if (continueOnError) { - errors.push(error); - continue; - } else { - throw error; - } - } + const createdItemsInOrder: Array< + Awaited> + > = []; - const itemClass = new ItemClass(); - const itemCategoryClass = new ItemCategory(); - category = - (await itemCategoryClass.findItemCategory({ name: item.type })) || null; - if (!category) { - category = await itemCategoryClass.create({ name: item.type }); + function* itemIterator() { + for (let idx = 0; idx < rawItems.length; idx++) { + const item = transform(rawItems[idx]); + yield { ...item, category: item.type, imageUrls: item.image_urls }; } + } - const newItem = await itemClass.create({ - name: item.name, - weight: convertWeight( - Number(item.weight), - item.unit as any, - SMALLEST_WEIGHT_UNIT, - ), - unit: item.unit, - categoryId: category.id, - global: true, - ownerId: item.ownerId, - sku: item.sku, - productUrl: item.productUrl, - description: item.description, - productDetails: item.productDetails, - seller: item.seller, - }); - - createdItemsInOrder.push([idx, newItem]); - - if (item.image_urls) { - const urls = item.image_urls.split(','); - - const newItemImages = []; - for (const url of urls) { - newItemImages.push({ - itemId: newItem.id, - url, - }); + await bulkAddNewItemsService({ + items: itemIterator(), + executionCtx, + onItemCreationError: (error) => { + if (!continueOnError) { + throw error; } + errors.push(error); + }, + }); - await DbClient.instance - .insert(itemImageTable) - .values(newItemImages) - .run(); - } - } - - // Format Item for vector indexes - const vectorData = []; - for (const [idx, item] of createdItemsInOrder) { - item.category = { name: transform(rawItems[idx]).type }; - vectorData.push({ - id: item.id, - content: summarizeItem(item), - namespace: ITEM_TABLE_NAME, - metadata: { - isPublic: item.global, - ownerId: item.ownerId, - }, - }); - } - executionCtx.waitUntil(VectorClient.instance.syncRecords(vectorData)); + return { + createdItemsInOrder, + errors, + }; }; diff --git a/server/src/services/item/addItemService.ts b/server/src/services/item/addItemService.ts index 6488095e9..eb9344dfd 100644 --- a/server/src/services/item/addItemService.ts +++ b/server/src/services/item/addItemService.ts @@ -1,12 +1,17 @@ -// import { prisma } from '../../prisma'; -import { Item } from '../../drizzle/methods/Item'; +import { type ExecutionContext } from 'hono'; +import * as validator from '@packrat/validations'; +import { Item as ItemClass } from '../../drizzle/methods/Item'; import { ItemCategory } from '../../drizzle/methods/itemcategory'; import { ItemOwners } from '../../drizzle/methods/ItemOwners'; import { ItemCategory as categories } from '../../utils/itemCategory'; -import { type InsertItemCategory } from '../../db/schema'; +import { + Item, + ITEM_TABLE_NAME, + type InsertItemCategory, +} from '../../db/schema'; import { VectorClient } from '../../vector/client'; import { convertWeight, SMALLEST_WEIGHT_UNIT } from 'src/utils/convertWeight'; -import { type ExecutionContext } from 'hono'; +import { summarizeItem } from 'src/utils/item'; /** * Generates a new item and adds it to a pack based on the given parameters. @@ -34,7 +39,7 @@ export const addItemService = async ( throw new Error(`Category must be one of: ${categories.join(', ')}`); } const itemCategoryClass = new ItemCategory(); - const itemClass = new Item(); + const itemClass = new ItemClass(); const itemOwnersClass = new ItemOwners(); category = (await itemCategoryClass.findItemCategory({ name: type })) || null; if (!category) { @@ -87,3 +92,109 @@ export const addItemService = async ( return item; }; + +/** + * Creates a new item. + * @param {Object} item - The data for the new item. + * @return {object} An object containing the newly created item + */ +export const addNewItemService = async ( + item: typeof validator.addNewItemGlobal._type, + ownerId: string, +): Promise => { + let category: InsertItemCategory | null; + if (!categories.includes(item.category)) { + const error = new Error( + `[${item.sku}#${item.name}]: Category must be one of: ${categories.join(', ')}`, + ); + throw error; + } + + const itemClass = new ItemClass(); + const itemCategoryClass = new ItemCategory(); + category = + (await itemCategoryClass.findItemCategory({ name: item.category })) || null; + if (!category) { + category = await itemCategoryClass.create({ name: item.category }); + } + + const newItem = await itemClass.create({ + name: item.name, + weight: convertWeight( + Number(item.weight), + item.unit as any, + SMALLEST_WEIGHT_UNIT, + ), + unit: item.unit, + categoryId: category.id, + global: true, + ownerId, + sku: item.sku, + productUrl: item.productUrl, + description: item.description, + productDetails: item.productDetails, + seller: item.seller, + }); + + if (item.imageUrls) { + const urls = item.imageUrls.split(','); + for (const url of urls) { + await itemClass.insertImage(newItem.id, url); + } + } + + return { ...newItem, category }; +}; + +/** + * Adds a list of items to the global inventory and indexes them in the vector database. + * @param {Object} bulkPram: the config parameter + * @param {Object} bulkPram.items - The list of items to add. + * @param {string} bulkPram.ownerId - The owner of all items + * @param {ExecutionContext} bulkParam.onItemCreated - A callback function to be called when an item is created. + * @param {Function} bulkParam.onItemCreated - A callback function to be called when an item is created. + */ +export const bulkAddNewItemsService = async (bulkParam: { + items: Iterable< + typeof validator.addNewItemGlobal._type & { ownerId: string } + >; + executionCtx: ExecutionContext; + onItemCreated?: ( + item: Awaited>, + bulkIndex: number, + ) => void; + onItemCreationError?: (error: Error, bulkIndex: number) => void; +}): Promise>>> => { + const { items, executionCtx, onItemCreated, onItemCreationError } = bulkParam; + const createdItems: Array>> = []; + const vectorData = []; + + let idx = -1; + for (const item of items) { + idx += 1; + try { + const createdItem = await addNewItemService(item, item.ownerId); + if (onItemCreated) { + onItemCreated(createdItem, idx); + } + createdItems.push(createdItem); + + vectorData.push({ + id: createdItem.id, + content: summarizeItem(createdItem), + namespace: ITEM_TABLE_NAME, + metadata: { + isPublic: createdItem.global, + ownerId: createdItem.ownerId, + }, + }); + } catch (error) { + if (onItemCreationError) { + onItemCreationError(error as Error, idx); + } + } + } + + executionCtx.waitUntil(VectorClient.instance.syncRecords(vectorData)); + return createdItems; +}; diff --git a/server/src/services/packTemplate/addPackTemplateService.ts b/server/src/services/packTemplate/addPackTemplateService.ts index 4ced9cb47..49549c619 100644 --- a/server/src/services/packTemplate/addPackTemplateService.ts +++ b/server/src/services/packTemplate/addPackTemplateService.ts @@ -1,6 +1,13 @@ +import type { ExecutionContext } from 'hono'; +import * as validator from '@packrat/validations'; + import { PackTemplate as PackTemplateClass } from '../../drizzle/methods/PackTemplate'; import * as ItemPackTemplateService from '../itemPackTemplate/itemPackTemplate.service'; -import { PackTemplate } from 'src/db/schema'; +import * as ItemService from '../item/item.service'; +import { ITEM_TABLE_NAME, PackTemplate } from 'src/db/schema'; +import { summarizeItem } from 'src/utils/item'; +import { VectorClient } from 'src/vector/client'; +import { i } from 'vitest/dist/reporters-QGe8gs4b.js'; /** * Adds a new pack template service. @@ -10,15 +17,10 @@ import { PackTemplate } from 'src/db/schema'; * @param {string} packTemplateData.type - Whether the pack is public or not. * @return {Object} An object containing the created pack. */ -export const addPackTemplateService = async (packTemplateData: { - name: string; - description: string; - type: string; - itemPackTemplates: Array<{ - itemId: string; - quantity: number; - }>; -}): Promise => { +export const addPackTemplateService = async ( + packTemplateData: typeof validator.addPackTemplate._type, + executionCtx: ExecutionContext, +): Promise => { const { name, description, type } = packTemplateData; console.log({ packTemplateData }); const packTemplateClass = new PackTemplateClass(); @@ -37,17 +39,29 @@ export const addPackTemplateService = async (packTemplateData: { ); } - for (const itemPackTemplate of packTemplateData.itemPackTemplates) { - try { + function* itemIterator() { + for (const itemPackTemplate of packTemplateData.itemPackTemplates) { + yield { + ...itemPackTemplate.item, + ownerId: packTemplateData.itemsOwnerId, + }; + } + } + + await ItemService.bulkAddNewItemsService({ + items: itemIterator(), + executionCtx, + onItemCreationError: (error, idx) => { + console.error(`Error creating item at ${idx}:`, error); + }, + onItemCreated: async (createdItem, idx) => { await ItemPackTemplateService.addItemPackTemplate({ - itemId: itemPackTemplate.itemId, - quantity: itemPackTemplate.quantity, + itemId: createdItem.id, + quantity: packTemplateData.itemPackTemplates[idx].quantity, packTemplateId: existingPack.id, }); - } catch (error) { - console.log(error); - } - } + }, + }); return existingPack; }; From 6f2c05bc02fcab01c1a9acea5da6f0d76c8f6118 Mon Sep 17 00:00:00 2001 From: Tadjaur Date: Thu, 12 Dec 2024 23:50:06 +0100 Subject: [PATCH 4/4] Update and use existing item definition --- .../src/validations/itemRoutesValidator.ts | 26 ++- .../packTemplateRoutesValidator.ts | 8 +- .../src/controllers/item/importItemsGlobal.ts | 111 +++++++----- .../packTemplates/getPackTemplate.ts | 4 +- server/src/controllers/user/getUsers.ts | 4 +- server/src/db/schema.ts | 5 - .../src/services/item/addItemGlobalService.ts | 166 ++++++------------ server/src/services/item/addItemService.ts | 114 +----------- .../services/item/bulkAddGlobalItemService.ts | 147 +++++----------- .../packTemplate/addPackTemplateService.ts | 6 +- server/src/tests/routes/packTemplate.spec.ts | 4 + 11 files changed, 193 insertions(+), 402 deletions(-) diff --git a/packages/validations/src/validations/itemRoutesValidator.ts b/packages/validations/src/validations/itemRoutesValidator.ts index 0e0ba3c2a..95eb2bb6f 100644 --- a/packages/validations/src/validations/itemRoutesValidator.ts +++ b/packages/validations/src/validations/itemRoutesValidator.ts @@ -66,25 +66,19 @@ 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 const addNewItemGlobal = z.object({ - name: z.string(), - weight: z.number(), - unit: z.string(), - category: z.enum(['Food', 'Water', 'Essentials']), - sku: z.string(), - productUrl: z.string(), - description: z.string(), - productDetails: z.record( - z.string(), - z.union([z.string(), z.number(), z.boolean()]), - ), - seller: z.string(), - imageUrls: z.string(), -}); +export type AddItemGlobalType = z.infer; export const importItemsGlobal = z.object({ content: z.string(), diff --git a/packages/validations/src/validations/packTemplateRoutesValidator.ts b/packages/validations/src/validations/packTemplateRoutesValidator.ts index 2458c9db5..de1c47541 100644 --- a/packages/validations/src/validations/packTemplateRoutesValidator.ts +++ b/packages/validations/src/validations/packTemplateRoutesValidator.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { addNewItemGlobal } from './itemRoutesValidator'; +import { addItemGlobal } from './itemRoutesValidator'; export const getPackTemplates = z.object({ filter: z @@ -23,7 +23,7 @@ export const getPackTemplate = z.union([ name: z.string(), id: z.string().optional(), }), -] as const); +]); export const createPackFromTemplate = z.object({ packTemplateId: z.string().min(1), @@ -37,10 +37,12 @@ export const addPackTemplate = z.object({ itemsOwnerId: z.string(), itemPackTemplates: z.array( z.object({ - item: addNewItemGlobal, + item: addItemGlobal.omit({ ownerId: true }), quantity: z.number(), }), ), }); +export type AddPackTemplateType = z.infer; + export const addPackTemplates = z.array(addPackTemplate); diff --git a/server/src/controllers/item/importItemsGlobal.ts b/server/src/controllers/item/importItemsGlobal.ts index fc15e1de9..50014a0e0 100644 --- a/server/src/controllers/item/importItemsGlobal.ts +++ b/server/src/controllers/item/importItemsGlobal.ts @@ -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'; @@ -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>} csvRawItems - The raw CSV items. + * @param {string} ownerId - The ID of the owner. + * @returns {Iterable} An iterable that yields the validated items. + */ +function* sanitizeItemsIterator( + csvRawItems: Array>, + ownerId: string, +): Generator { + 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', @@ -114,49 +169,23 @@ export function importItemsGlobalRoute() { results.data.pop(); } - let idx = 0; - await addItemGlobalServiceBatch( - results.data, - true, + const errors: Error[] = []; + const createdItems = await bulkAddItemsGlobalService( + sanitizeItemsIterator(results.data, ownerId), opts.ctx.executionCtx, - (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), - }; + { + 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}`)); diff --git a/server/src/controllers/packTemplates/getPackTemplate.ts b/server/src/controllers/packTemplates/getPackTemplate.ts index 69eeda2e2..03942e002 100644 --- a/server/src/controllers/packTemplates/getPackTemplate.ts +++ b/server/src/controllers/packTemplates/getPackTemplate.ts @@ -1,9 +1,9 @@ import * as validator from '@packrat/validations'; -import { publicProcedure } from 'src/trpc'; +import { protectedProcedure } from 'src/trpc'; import { getPackTemplateService } from 'src/services/packTemplate/packTemplate.service'; export function getPackTemplateRoute() { - return publicProcedure + return protectedProcedure .input(validator.getPackTemplate) .query(async ({ input }) => { const param = input.id ? { id: input.id } : { name: input.name }; diff --git a/server/src/controllers/user/getUsers.ts b/server/src/controllers/user/getUsers.ts index ece62d01e..6721b0655 100644 --- a/server/src/controllers/user/getUsers.ts +++ b/server/src/controllers/user/getUsers.ts @@ -1,4 +1,4 @@ -import { protectedProcedure, publicProcedure } from '../../trpc'; +import { protectedProcedure } from '../../trpc'; import { responseHandler } from '../../helpers/responseHandler'; import { User } from '../../drizzle/methods/User'; @@ -13,7 +13,7 @@ export const getUsers = async (c) => { }; export function getUsersRoute() { - return publicProcedure.query(async (opts) => { + return protectedProcedure.query(async (opts) => { const userClass = new User(); const users = await userClass.findMany(); return users; diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index fc34ccde2..41b40f2fb 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -613,15 +613,10 @@ export const selectTemplateSchema = createSelectSchema(template); export type PackTemplate = InferSelectModel; export type InsertPackTemplate = InferInsertModel; -export const insertPackTemplateSchema = createInsertSchema(packTemplate); export const selectPackTemplateSchema = createSelectSchema(packTemplate); export type ItemPackTemplate = InferSelectModel; export type InsertItemPackTemplate = InferInsertModel; -export const insertItemPackTemplateSchema = - createInsertSchema(itemPackTemplate); -export const selectItemPackTemplateSchema = - createSelectSchema(itemPackTemplate); export type Pack = InferSelectModel; export type InsertPack = InferInsertModel; diff --git a/server/src/services/item/addItemGlobalService.ts b/server/src/services/item/addItemGlobalService.ts index 567c8390b..b9371af67 100644 --- a/server/src/services/item/addItemGlobalService.ts +++ b/server/src/services/item/addItemGlobalService.ts @@ -1,4 +1,5 @@ import { type ExecutionContext } from 'hono'; +import * as validator from '@packrat/validations'; import { Item, ITEM_TABLE_NAME, @@ -9,145 +10,76 @@ import { ItemCategory } from '../../drizzle/methods/itemcategory'; import { ItemCategory as categories } from '../../utils/itemCategory'; import { VectorClient } from '../../vector/client'; import { convertWeight, SMALLEST_WEIGHT_UNIT } from 'src/utils/convertWeight'; -import { DbClient } from 'src/db/client'; -import { itemImage as itemImageTable } from '../../db/schema'; import { summarizeItem } from 'src/utils/item'; -import { addNewItemService, bulkAddNewItemsService } from './addItemService'; -// import { prisma } from '../../prisma'; -interface AddItemGlobalServiceParams { - /** The name of the item. */ - name: string; - /** The description of the item. */ - description?: string; - /** The weight of the item. */ - weight: number; - /** The unit of measurement for the item. */ - unit: string; - /** The category of the item. */ - type: (typeof categories)[number]; - /** The ID of the owner of the item. */ - ownerId: string; - /** The URLs of the images of the item. */ - image_urls?: string; - /** The SKU of the item. */ - sku?: string; - /** The URL of the product of the item. */ - productUrl?: string; - /** The product details of the item. */ - productDetails?: Record; - /** The seller of the item. */ - seller?: string; -} +type ItemWithCategory = Item & { category?: InsertItemCategory }; /** * Adds an item to the global service. + * @param {Object} item - The data for the new item. + * @param {ExecutionContext} executionCtx - Then vector database execution context * @return {Promise} The newly created item. */ export const addItemGlobalService = async ( - { - name, - weight, - unit, - type, - ownerId, - image_urls, - sku, - productUrl, - description, - productDetails, - seller, - }: AddItemGlobalServiceParams, - executionCtx: ExecutionContext, -) => { + item: validator.AddItemGlobalType, + executionCtx?: ExecutionContext, +): Promise => { let category: InsertItemCategory | null; - if (!categories.includes(type)) { - throw new Error(`Category must be one of: ${categories.join(', ')}`); + if (!categories.includes(item.type)) { + const error = new Error( + `[${item.sku}#${item.name}]: Category must be one of: ${categories.join(', ')}`, + ); + throw error; } + const itemClass = new ItemClass(); const itemCategoryClass = new ItemCategory(); - category = (await itemCategoryClass.findItemCategory({ name: type })) || null; + category = + (await itemCategoryClass.findItemCategory({ name: item.type })) || null; if (!category) { - category = await itemCategoryClass.create({ name: type }); + category = await itemCategoryClass.create({ name: item.type }); } - const newItem = await itemClass.create({ - name, - weight: convertWeight(Number(weight), unit as any, SMALLEST_WEIGHT_UNIT), - unit, + + const newItem = (await itemClass.create({ + name: item.name, + weight: convertWeight( + Number(item.weight), + item.unit as any, + SMALLEST_WEIGHT_UNIT, + ), + unit: item.unit, categoryId: category.id, global: true, - ownerId, - sku, - productUrl, - description, - productDetails, - seller, - }); + ownerId: item.ownerId, + sku: item.sku, + productUrl: item.productUrl, + description: item.description, + productDetails: item.productDetails, + seller: item.seller, + })) as ItemWithCategory; - if (image_urls) { - const urls = image_urls.split(','); + if (item.image_urls) { + const urls = item.image_urls.split(','); for (const url of urls) { - const newItemImage = { - itemId: newItem.id, - url, - }; - await DbClient.instance.insert(itemImageTable).values(newItemImage).run(); + await itemClass.insertImage(newItem.id, url); } } - executionCtx.waitUntil( - VectorClient.instance.syncRecord({ - id: newItem.id, - content: `product_name: ${name}, category: ${type}, description: ${description}, productDetails: ${JSON.stringify( - productDetails, - )}`, - namespace: ITEM_TABLE_NAME, - metadata: { - isPublic: newItem.global, - ownerId, - }, - }), - ); + newItem.category = category; - return newItem; -}; - -/** - * Adds list of items to the global service. - * @return {Promise} The newly created item. - */ -export const addItemGlobalServiceBatch = async ( - rawItems: T[], - continueOnError = false, - executionCtx: ExecutionContext, - transform: (rawItem: T) => AddItemGlobalServiceParams, -) => { - const errors: Error[] = []; - - const createdItemsInOrder: Array< - Awaited> - > = []; - - function* itemIterator() { - for (let idx = 0; idx < rawItems.length; idx++) { - const item = transform(rawItems[idx]); - yield { ...item, category: item.type, imageUrls: item.image_urls }; - } + if (executionCtx) { + executionCtx.waitUntil( + VectorClient.instance.syncRecord({ + id: newItem.id, + content: summarizeItem(newItem), + namespace: ITEM_TABLE_NAME, + metadata: { + isPublic: newItem.global, + ownerId: newItem.ownerId, + }, + }), + ); } - await bulkAddNewItemsService({ - items: itemIterator(), - executionCtx, - onItemCreationError: (error) => { - if (!continueOnError) { - throw error; - } - errors.push(error); - }, - }); - - return { - createdItemsInOrder, - errors, - }; + return newItem; }; diff --git a/server/src/services/item/addItemService.ts b/server/src/services/item/addItemService.ts index eb9344dfd..59f0652d2 100644 --- a/server/src/services/item/addItemService.ts +++ b/server/src/services/item/addItemService.ts @@ -1,17 +1,11 @@ import { type ExecutionContext } from 'hono'; -import * as validator from '@packrat/validations'; import { Item as ItemClass } from '../../drizzle/methods/Item'; import { ItemCategory } from '../../drizzle/methods/itemcategory'; import { ItemOwners } from '../../drizzle/methods/ItemOwners'; import { ItemCategory as categories } from '../../utils/itemCategory'; -import { - Item, - ITEM_TABLE_NAME, - type InsertItemCategory, -} from '../../db/schema'; +import { type InsertItemCategory } from '../../db/schema'; import { VectorClient } from '../../vector/client'; import { convertWeight, SMALLEST_WEIGHT_UNIT } from 'src/utils/convertWeight'; -import { summarizeItem } from 'src/utils/item'; /** * Generates a new item and adds it to a pack based on the given parameters. @@ -92,109 +86,3 @@ export const addItemService = async ( return item; }; - -/** - * Creates a new item. - * @param {Object} item - The data for the new item. - * @return {object} An object containing the newly created item - */ -export const addNewItemService = async ( - item: typeof validator.addNewItemGlobal._type, - ownerId: string, -): Promise => { - let category: InsertItemCategory | null; - if (!categories.includes(item.category)) { - const error = new Error( - `[${item.sku}#${item.name}]: Category must be one of: ${categories.join(', ')}`, - ); - throw error; - } - - const itemClass = new ItemClass(); - const itemCategoryClass = new ItemCategory(); - category = - (await itemCategoryClass.findItemCategory({ name: item.category })) || null; - if (!category) { - category = await itemCategoryClass.create({ name: item.category }); - } - - const newItem = await itemClass.create({ - name: item.name, - weight: convertWeight( - Number(item.weight), - item.unit as any, - SMALLEST_WEIGHT_UNIT, - ), - unit: item.unit, - categoryId: category.id, - global: true, - ownerId, - sku: item.sku, - productUrl: item.productUrl, - description: item.description, - productDetails: item.productDetails, - seller: item.seller, - }); - - if (item.imageUrls) { - const urls = item.imageUrls.split(','); - for (const url of urls) { - await itemClass.insertImage(newItem.id, url); - } - } - - return { ...newItem, category }; -}; - -/** - * Adds a list of items to the global inventory and indexes them in the vector database. - * @param {Object} bulkPram: the config parameter - * @param {Object} bulkPram.items - The list of items to add. - * @param {string} bulkPram.ownerId - The owner of all items - * @param {ExecutionContext} bulkParam.onItemCreated - A callback function to be called when an item is created. - * @param {Function} bulkParam.onItemCreated - A callback function to be called when an item is created. - */ -export const bulkAddNewItemsService = async (bulkParam: { - items: Iterable< - typeof validator.addNewItemGlobal._type & { ownerId: string } - >; - executionCtx: ExecutionContext; - onItemCreated?: ( - item: Awaited>, - bulkIndex: number, - ) => void; - onItemCreationError?: (error: Error, bulkIndex: number) => void; -}): Promise>>> => { - const { items, executionCtx, onItemCreated, onItemCreationError } = bulkParam; - const createdItems: Array>> = []; - const vectorData = []; - - let idx = -1; - for (const item of items) { - idx += 1; - try { - const createdItem = await addNewItemService(item, item.ownerId); - if (onItemCreated) { - onItemCreated(createdItem, idx); - } - createdItems.push(createdItem); - - vectorData.push({ - id: createdItem.id, - content: summarizeItem(createdItem), - namespace: ITEM_TABLE_NAME, - metadata: { - isPublic: createdItem.global, - ownerId: createdItem.ownerId, - }, - }); - } catch (error) { - if (onItemCreationError) { - onItemCreationError(error as Error, idx); - } - } - } - - executionCtx.waitUntil(VectorClient.instance.syncRecords(vectorData)); - return createdItems; -}; diff --git a/server/src/services/item/bulkAddGlobalItemService.ts b/server/src/services/item/bulkAddGlobalItemService.ts index d60ed3f40..c0aa1bb49 100644 --- a/server/src/services/item/bulkAddGlobalItemService.ts +++ b/server/src/services/item/bulkAddGlobalItemService.ts @@ -1,111 +1,60 @@ import { type ExecutionContext } from 'hono'; -import { type InsertItemCategory } from '../../db/schema'; -import { ItemCategory } from '../../drizzle/methods/itemcategory'; -import { DbClient } from 'src/db/client'; -import { - item as ItemTable, - itemImage as itemImageTable, -} from '../../db/schema'; -import { convertWeight, SMALLEST_WEIGHT_UNIT } from 'src/utils/convertWeight'; -import { eq } from 'drizzle-orm'; +import * as validator from '@packrat/validations'; +import { ITEM_TABLE_NAME } from '../../db/schema'; import { VectorClient } from 'src/vector/client'; - +import { addItemGlobalService } from './addItemGlobalService'; +import { summarizeItem } from 'src/utils/item'; + +/** + * Adds a list of items to the global inventory and indexes them in the vector database. + * @param {Object} items - The list of items to add. + * @param {string} executionCtx - The execution context. + * @param {ExecutionContext} callbacks.onItemCreated - A callback function to be called when an item is created. + * @param {Function} callbacks.onItemCreationError - A callback function to be called when an item creation fails. + * @return {Promise} A promise that resolves to an array of the created items. + */ export const bulkAddItemsGlobalService = async ( - items: Array<{ - name: string; - weight: number; - unit: string; - type: 'Food' | 'Water' | 'Essentials'; - ownerId: string; - image_urls?: string; - sku?: string; - productUrl?: string; - description?: string; - productDetails?: { - [key: string]: string | number | boolean | null; - }; - seller?: string; - }>, + items: Iterable, executionCtx: ExecutionContext, -) => { - const categories = ['Food', 'Water', 'Essentials']; - - const itemCategoryClass = new ItemCategory(); - const insertedItems = []; - - for (const itemData of items) { - const { name, weight, unit, type, ownerId, image_urls } = itemData; - if (!categories.includes(type)) { - throw new Error(`Category must be one of: ${categories.join(', ')}`); - } - - let category: InsertItemCategory | null; - category = - (await itemCategoryClass.findItemCategory({ name: type })) || null; - if (!category) { - category = await itemCategoryClass.create({ name: type }); - } - - // Check if item with the same name already exists - const existingItem = await DbClient.instance - .select() - .from(ItemTable) - .where(eq(ItemTable.name, name)) - .get(); - - if (existingItem) { - continue; - } - - const newItem = { - name, - weight: convertWeight(Number(weight), unit as any, SMALLEST_WEIGHT_UNIT), - unit, - categoryId: category.id, - global: true, - ownerId, - sku, - productUrl, - description, - productDetails, - seller, - }; - - const item = await DbClient.instance - .insert(ItemTable) - .values(newItem) - .returning() - .get(); + callbacks?: { + onItemCreated?: ( + item: Awaited>, + bulkIndex: number, + ) => void; + onItemCreationError?: (error: Error, bulkIndex: number) => void; + }, +): Promise>>> => { + const { onItemCreated, onItemCreationError } = callbacks; + const createdItems: Array>> = + []; + const vectorData = []; + + let idx = -1; + for (const item of items) { + idx += 1; + try { + const createdItem = await addItemGlobalService(item); + if (onItemCreated) { + onItemCreated(createdItem, idx); + } + createdItems.push(createdItem); - executionCtx.waitUntil( - VectorClient.instance.syncRecord({ - id: item.id, - content: name, - namespace: 'items', + vectorData.push({ + id: createdItem.id, + content: summarizeItem(createdItem), + namespace: ITEM_TABLE_NAME, metadata: { - isPublic: item.global, - ownerId, + isPublic: createdItem.global, + ownerId: createdItem.ownerId, }, - }), - ); - - if (image_urls) { - const urls = image_urls.split(','); - for (const url of urls) { - const newItemImage = { - itemId: item.id, - url, - }; - await DbClient.instance - .insert(itemImageTable) - .values(newItemImage) - .run(); + }); + } catch (error) { + if (onItemCreationError) { + onItemCreationError(error as Error, idx); } - console.log('Added image urls for item:', item.id); } - - insertedItems.push(item); } - return insertedItems; + executionCtx.waitUntil(VectorClient.instance.syncRecords(vectorData)); + return createdItems; }; diff --git a/server/src/services/packTemplate/addPackTemplateService.ts b/server/src/services/packTemplate/addPackTemplateService.ts index 49549c619..637a8083c 100644 --- a/server/src/services/packTemplate/addPackTemplateService.ts +++ b/server/src/services/packTemplate/addPackTemplateService.ts @@ -18,7 +18,7 @@ import { i } from 'vitest/dist/reporters-QGe8gs4b.js'; * @return {Object} An object containing the created pack. */ export const addPackTemplateService = async ( - packTemplateData: typeof validator.addPackTemplate._type, + packTemplateData: validator.AddPackTemplateType, executionCtx: ExecutionContext, ): Promise => { const { name, description, type } = packTemplateData; @@ -48,9 +48,7 @@ export const addPackTemplateService = async ( } } - await ItemService.bulkAddNewItemsService({ - items: itemIterator(), - executionCtx, + await ItemService.bulkAddItemsGlobalService(itemIterator(), executionCtx, { onItemCreationError: (error, idx) => { console.error(`Error creating item at ${idx}:`, error); }, diff --git a/server/src/tests/routes/packTemplate.spec.ts b/server/src/tests/routes/packTemplate.spec.ts index 5af0be490..d914ee981 100644 --- a/server/src/tests/routes/packTemplate.spec.ts +++ b/server/src/tests/routes/packTemplate.spec.ts @@ -123,6 +123,10 @@ describe('Pack template routes', () => { name: 'test', description: 'pack template description', type: 'pack', + itemPackTemplates: packTemplateItems.map((item) => ({ + itemId: item.id, + quantity: 1, + })), }); expect(packTemplate).toMatchObject([ { ...packTemplate, items: packTemplateItems },