# dalpuri Socket.IO data service that exposes Prisma collectors over event-based RPC. Primary use case: - A remote system connects over Socket.IO. - It authenticates using a pre-shared key (PSK). - It invokes collector events (for example, fetchCompanies) with optional query options. - It receives normalized success or error responses. ## Stack - Runtime: Bun - Transport: Socket.IO - Database: Microsoft SQL Server (READ-ONLY) - ORM: Prisma with MSSQL adapter ## ⚠️ Important: Database is READ-ONLY **All database operations are SELECT queries only.** No INSERT, UPDATE, DELETE, or other write operations are allowed. This service provides read-only access to ConnectWise Manage data. ## Schema Corrections Applied All product-related tables have been corrected to map to the actual database schema: | Prisma Model | Corrected Table | Previous (Incorrect) | | ------------------- | ------------------------ | --------------------------- | | ProductCategory | `IV_Category` | ~~Product_Category~~ | | ProductSubcategory | `IV_SubCategory` | ~~Product_Subcategory~~ | | ProductManufacturer | `Manufacturer` | ~~Product_Manufacturer~~ | | ProductCatalog | `IV_Item` | ~~Product_Catalog~~ | | ProductInventory | `Inventory_By_Warehouse` | ~~Product_Inventory~~ | | ItemVendor | `iv_item_Vendor` | ~~ProductVendor~~ (removed) | **Key Fixes:** - ✅ All `@@map` directives point to actual database tables - ✅ All field names match database columns exactly - ✅ Primary keys use correct RecID naming (e.g., `ivItemRecId` not `catalogRecId`) - ✅ Removed incorrect `ProductVendor` model (table doesn't exist) - ✅ Added proper `ItemVendor` junction table model - ✅ Fixed TypeScript types (`@db.UniqueIdentifier` capitalization) - ✅ Corrected all relationship hierarchies ## Database Relations Reference ### Product Models #### ProductCategory (`IV_Category`) **Primary Key:** `ivCatRecId` **Available Relations:** - `subcategories` → ProductSubcategory[] - All subcategories in this category **Example Include:** ```typescript { include: ["subcategories"]; } ``` --- #### ProductSubcategory (`IV_SubCategory`) **Primary Key:** `ivSubcatRecId` **Foreign Keys:** `ivCatRecId` → ProductCategory **Available Relations:** - `category` → ProductCategory - Parent category - `products` → ProductCatalog[] - All products in this subcategory **Example Include:** ```typescript { include: ["category", "products"]; } ``` --- #### ProductManufacturer (`Manufacturer`) **Primary Key:** `manufacturerRecId` **Available Relations:** - `products` → ProductCatalog[] - All products by this manufacturer **Example Include:** ```typescript { include: ["products"]; } ``` --- #### ProductCatalog (`IV_Item`) **Primary Key:** `ivItemRecId` **Foreign Keys:** `ivSubcatRecId` → ProductSubcategory, `manufacturerRecId` → ProductManufacturer **Available Relations:** - `subcategory` → ProductSubcategory - Category info (can nest `category` for full hierarchy) - `manufacturer` → ProductManufacturer - Manufacturer info - `inventory` → ProductInventory[] - All inventory records across warehouses - `itemVendors` → ItemVendor[] - All vendor relationships **Example Include:** ```typescript { include: ["subcategory", "manufacturer", "inventory", "itemVendors"]; } ``` **Nested Include for Full Category Hierarchy:** ```json { "include": { "subcategory": { "include": { "category": true } } } } ``` --- #### ProductInventory (`Inventory_By_Warehouse`) **Primary Key:** `inventoryByWarehouseRecId` **Foreign Keys:** `ivItemRecId` → ProductCatalog **Available Relations:** - `productCatalog` → ProductCatalog - Parent product **Example Include:** ```typescript { include: ["productCatalog"]; } ``` --- #### ItemVendor (`iv_item_Vendor`) **Primary Key:** `ivItemVendorId` **Foreign Keys:** `ivItemRecId` → ProductCatalog **Available Relations:** - `product` → ProductCatalog - Product this vendor supplies **Example Include:** ```typescript { include: ["product"]; } ``` --- ### Company Models #### Company **Primary Key:** `companyRecId` **Available Relations:** - `timeZone` → TimeZone - `companyStatus` → CompanyStatus - `taxCode` → TaxCode - `currency` → Currency - `ownerLevel` → OwnerLevel - `market` → Market - `ownershipType` → OwnershipType - `billingTerms` → BillingTerms - `billingDelivery` → BillingDelivery - `srSignoff` → SrSignoff - `ivPriceHeader` → IvPriceHeader - `officeCalendar` → OfficeCalendar - `customerUsageType` → CustomerUsageType - `blInvtemplateSetup` → BlInvTemplateSetup - `emailTemplate` → EmailTemplate - `parentCompany` → Company - Parent company (self-relation) - `childCompanies` → Company[] - Child companies - `opportunityCompanies` → SoOpportunity[] - Opportunities where this is the primary company - `opportunityShipToCompanies` → SoOpportunity[] - Opportunities shipping to this company - `opportunityBillToCompanies` → SoOpportunity[] - Opportunities billing to this company **Example Include:** ```typescript { include: [ "companyStatus", "ownerLevel", "opportunityCompanies", "parentCompany", "childCompanies", ]; } ``` --- ### Opportunity Models #### SoOpportunity (`SO_Opportunity`) **Primary Key:** `opportunityRecId` **Available Relations:** - `company` → Company - Primary company - `contact` → Contact - Primary contact - `soPipeline` → SoPipeline - Pipeline stage - `soInterest` → SoInterest - Interest level - `billingUnit` → BillingUnit - Billing unit - `contractType` → ContractType - Contract type - `companyAddress` → CompanyAddress - Company address - `soOppStatus` → SoOppStatus - Opportunity status - `pmProject` → PmProject - Related project - `ownerLevel` → OwnerLevel - Owner level - `soType` → SoType - Opportunity type - `marketingCampaign` → MarketingCampaign - Marketing campaign - `agrType` → AgrType - Agreement type - `srService` → SrService - Service type - `soUrgency` → SoUrgency - Urgency level - `approvedByMember` → Member - Member who approved - `rejectedByMember` → Member - Member who rejected - `shipToCompany` → Company - Ship to company - `shipToContact` → Contact - Ship to contact - `shipToCompanyAddress` → CompanyAddress - Ship to address - `billToCompany` → Company - Bill to company - `billToContact` → Contact - Bill to contact - `billToCompanyAddress` → CompanyAddress - Bill to address - `billingTerms` → BillingTerms - Billing terms - `taxCode` → TaxCode - Tax code - `currency` → Currency - Currency - `techContact` → Contact - Technical contact **Example Include:** ```typescript { include: [ "company", "contact", "soPipeline", "soOppStatus", "ownerLevel", "approvedByMember", ]; } ``` --- ### Member Models #### Member **Primary Key:** `memberRecId` **Available Relations:** - `ownerLevel` → OwnerLevel - Member owner level - `billingUnit` → BillingUnit - Billing unit - `activityClass` → ActivityClass - Activity class - `memberType` → MemberType - Member type - `srOwnerLevel` → OwnerLevel - Service request owner level - `srBillingUnit` → BillingUnit - Service request billing unit - `schOwnerLevel` → OwnerLevel - Schedule owner level - `schBillingUnit` → BillingUnit - Schedule billing unit - `srBoard` → SrBoard - Service board - `timeZone` → TimeZone - Time zone - `brHeader` → BrHeader - BR header - `activityType` → ActivityType - Activity type - `pmOwnerLevel` → OwnerLevel - Project management owner level - `pmBillingUnit` → BillingUnit - Project management billing unit - `warehouse` → Warehouse - Warehouse - `warehouseBin` → WarehouseBin - Warehouse bin - `country` → Country - Country - `directionalSync` → DirectionalSync - Sync direction - `approvedOpportunities` → SoOpportunity[] - Opportunities approved by this member - `rejectedOpportunities` → SoOpportunity[] - Opportunities rejected by this member **Example Include:** ```typescript { include: [ "ownerLevel", "billingUnit", "memberType", "timeZone", "approvedOpportunities", ]; } ``` --- ### Contact Models #### Contact **Primary Key:** `contactRecId` **Available Relations:** - `opportunityContacts` → SoOpportunity[] - Opportunities where this is primary contact - `opportunityShipToContacts` → SoOpportunity[] - Opportunities shipping to this contact - `opportunityBillToContacts` → SoOpportunity[] - Opportunities billing to this contact - `opportunityTechContacts` → SoOpportunity[] - Opportunities where this is technical contact **Example Include:** ```typescript { include: ["opportunityContacts"]; } ``` --- ### Relationship Hierarchy Diagrams #### Product Hierarchy ``` ProductCategory (IV_Category) ↓ one-to-many ProductSubcategory (IV_SubCategory) ↓ one-to-many ProductCatalog (IV_Item) ←──── ProductManufacturer (Manufacturer) ↓ one-to-many many-to-one ↗ ├─→ ProductInventory (Inventory_By_Warehouse) └─→ ItemVendor (iv_item_Vendor) ``` #### Company/Opportunity Hierarchy ``` Company ├─→ opportunityCompanies (SoOpportunity[]) ├─→ parentCompany (Company) └─→ childCompanies (Company[]) SoOpportunity ├─→ company (Company) ├─→ contact (Contact) ├─→ soPipeline (SoPipeline) ├─→ soOppStatus (SoOppStatus) ├─→ ownerLevel (OwnerLevel) └─→ approvedByMember (Member) ``` ## Environment Variables Required variables: - DATABASE_URL: SQL Server connection string for Prisma - PSK: pre-shared key required for Socket.IO handshake - PORT: optional server port (defaults to 3000) Example shape: ```env DATABASE_URL="sqlserver://host:1433;database=your_db;user=your_user;password=your_password;encrypt=true;trustServerCertificate=true" PSK="replace-with-strong-shared-key" PORT="3000" ``` ## Install And Run Install dependencies: ```bash bun install ``` Run in watch mode: ```bash bun run dev ``` Generate Prisma client: ```bash bun run db:generate ``` View database in Prisma Studio: ```bash bun run db:studio ``` Type-check: ```bash bunx tsc --noEmit ``` ## Runtime Flow 1. Server starts from src/index.ts and calls startServer. 2. Socket.IO server is created. 3. Handshake authentication validates PSK. 4. On each new socket connection, server scans src/collectors. 5. For each .ts collector file, server registers an event named after the file name. 6. Incoming event payload is passed to the collector as opts. 7. Collector result is returned via callback in a standard envelope. ## Authentication Contract Connection is rejected unless provided PSK matches server PSK. Accepted handshake sources (first non-empty wins): - auth.psk - header x-psk - query psk Recommended client method: - send psk in auth.psk If auth fails: - socket connection is denied with Unauthorized ## Collector Event Contract ### Event Name Event name is the collector file name without .ts. Current collectors: - fetchCompanies (from src/collectors/fetchCompanies.ts) - fetchMembers (from src/collectors/fetchMembers.ts) - fetchProducts (from src/collectors/fetchProducts.ts) ### Event Signature Client emits: ```ts socket.emit(eventName, opts, callback); ``` Where: - eventName: string - opts: optional object - callback: function receiving envelope ### opts Shape Collectors accept optional opts: ```ts { select?: string[]; include?: string[]; } ``` Rules: - Omit opts entirely to fetch full records. - Use select to request specific fields. - Use include to request relations. - Do not provide both select and include in the same call. If both select and include are provided: - collector throws - callback returns success false with error message ### Response Envelope Success: ```json { "success": true, "data": [ ... ] } ``` Failure: ```json { "success": false, "error": "error message" } ``` ## Concrete Usage Examples ### 1) Fetch all companies ```ts socket.emit("fetchCompanies", undefined, (res) => { if (!res.success) return console.error(res.error); console.log(res.data); }); ``` ### 2) Fetch selected company fields ```ts socket.emit( "fetchCompanies", { select: ["companyId", "companyName"] }, (res) => { if (!res.success) return console.error(res.error); console.log(res.data); }, ); ``` ### 3) Fetch companies with opportunities relation ```ts socket.emit("fetchCompanies", { include: ["opportunityCompanies"] }, (res) => { if (!res.success) return console.error(res.error); console.log(res.data); }); ``` ### 4) Fetch selected member fields ```ts socket.emit( "fetchMembers", { select: ["memberId", "firstName", "lastName"] }, (res) => { if (!res.success) return console.error(res.error); console.log(res.data); }, ); ``` ### 5) Fetch all products ```ts socket.emit("fetchProducts", undefined, (res) => { if (!res.success) return console.error(res.error); console.log(res.data); }); ``` ### 6) Fetch selected product fields ```ts socket.emit( "fetchProducts", { select: [ "ivItemRecId", "itemId", "description", "listPrice", "currentCost", ], }, (res) => { if (!res.success) return console.error(res.error); console.log(res.data); }, ); ``` ### 7) Fetch products with subcategory, manufacturer, and inventory ```ts socket.emit( "fetchProducts", { include: ["subcategory", "manufacturer", "inventory", "itemVendors"] }, (res) => { if (!res.success) return console.error(res.error); console.log(res.data); }, ); ``` ### 8) Fetch products with nested category hierarchy ```ts // To get the full category hierarchy, you can nest includes in the collector // This would require updating the collector to support nested includes socket.emit( "fetchProducts", { include: { subcategory: { include: { category: true, }, }, manufacturer: true, inventory: true, }, }, (res) => { if (!res.success) return console.error(res.error); console.log(res.data); }, ); ``` ## Reference Client (Node) This is the easiest template to hand to another system: ```ts import { io } from "socket.io-client"; const socket = io("http://localhost:3000", { auth: { psk: process.env.PSK }, transports: ["websocket"], }); socket.on("connect", () => { console.log("connected", socket.id); socket.emit("fetchCompanies", { select: ["companyId"] }, (res: any) => { if (!res.success) { console.error("collector error", res.error); return; } console.log("rows", res.data.length); }); }); socket.on("connect_error", (err) => { console.error("connect_error", err.message); }); ``` ## Collector Field Reference ### fetchCompanies Select fields: companyRecId, companyId, companyName, phoneNbr, websiteUrl, accountNbr, etc. Include relations: timeZone, companyStatus, taxCode, currency, ownerLevel, market, billingTerms, billingDelivery, opportunityCompanies, etc. ### fetchMembers Select fields: memberRecId, memberId, firstName, lastName, emailAddress, inactiveFlag, title, etc. Include relations: ownerLevel, billingUnit, memberType, timeZone, directionalSync, etc. ### fetchProducts Select fields: ivItemRecId, itemId, description, longDescription, ivSubcatRecId, manufacturerRecId, listPrice, currentCost, taxableFlag, inactiveFlag, mfgItemId, vendorSku, minimumStock, recurringFlag, lastUpdate, lastUpdateUtc, etc. Include relations: subcategory, manufacturer, inventory, itemVendors **Note:** The product model has been corrected to map to `IV_Item` table. Use the corrected field names as shown above. ## How To Add A New Collector 1. Create a new file in src/collectors, for example fetchOpportunities.ts. 2. Export a default async function with optional opts argument. 3. Use the helper in src/helper/collectorQuery.ts to process select/include. 4. Restart server if needed. Template: ```ts import { prisma } from "../constants"; import { Prisma } from "../../generated/prisma/client"; import { buildCollectorFindManyArgs, CollectorQueryOptions, } from "../helper/collectorQuery"; type OpportunityCollectorOpts = CollectorQueryOptions< Prisma.SoOpportunitySelect, Prisma.SoOpportunityInclude >; export default async (opts?: OpportunityCollectorOpts) => { const args: Prisma.SoOpportunityFindManyArgs = buildCollectorFindManyArgs< Prisma.SoOpportunitySelect, Prisma.SoOpportunityInclude >(opts); return prisma.soOpportunity.findMany(args); }; ``` ## Operational Notes - The server currently trusts all CORS origins at the Socket.IO layer. - Collector discovery happens at connection time by reading src/collectors. - Keep collector file names stable because they define public event names. - **Database is READ-ONLY**: Only SELECT queries are allowed. ## Generated Data Files The repository may contain generated JSON files (e.g., `products-with-relations.json`) that export sample data with all relationships included. These are useful for: - Understanding the data structure - Testing client applications - Offline development To regenerate product data: ```bash bun run export:products ``` ## Field Name Reference (Product Models) ### Old (Incorrect) vs New (Correct) Field Names | Old Field Name | New Field Name | Description | | --------------------- | ----------------------- | ----------------- | | `catalogRecId` | `ivItemRecId` | Primary key | | `cwCatalogId` | N/A (removed) | No longer exists | | `identifier` | `itemId` | Item identifier | | `productName` | `description` | Product name | | `price` | `listPrice` | List price | | `cost` | `currentCost` | Current cost | | `categoryRecId` | `ivSubcatRecId` | Subcategory FK | | `subcategoryRecId` | N/A (use ivSubcatRecId) | Changed | | `manufacturerPartNum` | `mfgItemId` | Manufacturer part | | `salesTaxableFlag` | `taxableFlag` | Tax flag | | `cwLastUpdated` | `lastUpdate` | Last update | ### Product Relations Name Changes | Old Relation | New Relation | Type | | ------------ | -------------- | ----------------------------------- | | `category` | `subcategory` | Changed (now points to subcategory) | | `vendor` | `itemVendors` | Changed (now junction table) | | N/A | `manufacturer` | Added | | `inventory` | `inventory` | Same (but different table) | **Important:** When updating collectors or client code, use the new field names and relations shown above. ## Troubleshooting ### Prisma Client Errors If you see Prisma client errors, regenerate the client: ```bash bun run db:generate ``` ### Connection Errors - Verify `DATABASE_URL` in `.env` - Check SQL Server connectivity - Ensure MSSQL adapter is installed (`@prisma/adapter-mssql`) ### Collector Errors - Check that relation names are correct (use the reference above) - Use `select` OR `include`, not both - Verify field names match the schema ## Additional Resources - [Prisma Documentation](https://www.prisma.io/docs) - [Socket.IO Documentation](https://socket.io/docs/v4/) - [ConnectWise Manage API Docs](https://developer.connectwise.com/)