Compare commits

..

14 Commits

Author SHA1 Message Date
HoloPanio 30b408e0db feat: add product to opportunity route, local product sequencing
- Add POST /v1/sales/opportunities/:identifier/products with field-level permission gating
- Add CWForecastItemCreate type for forecast item creation
- Store product display order locally (productSequence Int[] on Opportunity)
- Rewrite resequenceProducts to be local-only (no CW PUT, stable IDs)
- Remove reorderProducts CW util (PUT regenerated IDs & broke procurement)
- Update fetchProducts to apply local ordering with CW sequenceNumber fallback
- Add productSequence to OpportunityController.toJson()
- Update API_ROUTES.md, PERMISSIONS.md, PermissionNodes.ts
2026-03-01 18:01:02 -06:00
HoloPanio d7b374f8ab feat: sales activities, forecast products, catalog categories, member cache, procurement filters, and comprehensive tests
New features:
- ActivityController and manager for CW sales activities (CRUD)
- ForecastProductController for opportunity forecast/product lines
- CW member cache with dual-layer (in-memory + Redis) resolution
- Catalog category/subcategory/ecosystem taxonomy module
- Quote statuses type definitions with CW mapping
- User-defined fields (UDF) module with cache and event refresh
- Company sites CW module with serialization
- Procurement manager filters (category, ecosystem, manufacturer, price, stock)
- Opportunity notes CRUD and product line management via CW API
- Opportunity type definitions endpoint

Updates:
- OpportunityController: CW refresh, company hydration, activities, custom fields
- UserController: cwIdentifier field for CW member linking
- CatalogItemController: category/subcategory fields from CW
- PermissionNodes: sales note/product CRUD nodes, subCategories, collectPermissions
- API routes: procurement categories/filters, sales notes/products, opportunity types
- Global events: UDF and member refresh intervals on startup

Tests (414 passing):
- ActivityController, ForecastProductController, OpportunityController unit tests
- UserController cwIdentifier tests
- catalogCategories, companySites, memberCache, procurement module tests
- activityTypes, opportunityTypes, quoteStatuses type tests
- permissionNodes subCategories and getAllPermissionNodes tests
- Updated test setup with redis mock, API method mocks, and builder helpers
2026-03-01 13:19:00 -06:00
HoloPanio 883b648d5e fix: add identifier column migration and fix entrypoint resolve logic
- Add explicit migration for CatalogItem.identifier column
- Fix entrypoint script: resolve only migrations on 'Failed' lines (not all)
- Remove auto-diff generation (use committed migration files instead)
- Remove 2>/dev/null that swallowed migration errors
2026-02-27 17:44:08 -06:00
HoloPanio b787120461 fix: start HTTP server before background init to prevent bad gateway 2026-02-27 17:06:36 -06:00
HoloPanio 1326725995 fix: resolve failed migrations before deploying 2026-02-27 16:26:31 -06:00
HoloPanio 508fa39835 fix: crash loop recovery, auto-migrations, CI test pipeline
- Wrap startup syncs in safeStartup() to prevent crash on external service failure
- Add migrate-entrypoint.sh for auto-generating migrations from schema diff
- Update Dockerfile migration stage to use entrypoint script
- Add test job to build-and-publish workflow (runs before build)
- Add tests.yaml workflow to run tests on every push
- Fix test setup to use real RSA key pair instead of plain strings
- Add test script to package.json
2026-02-27 16:11:28 -06:00
HoloPanio b1f6462ac3 Fix UserController permission serialization and include current updates 2026-02-27 14:38:22 -06:00
HoloPanio 51eb36f4a6 fix: resolve type errors across test suite 2026-02-26 12:49:04 -06:00
HoloPanio 827b018f25 auto-create admin role on startup, use API_BASE_URL for auth redirects 2026-02-25 23:00:51 -06:00
HoloPanio 5852bd7819 remove migration drift check 2026-02-25 22:34:16 -06:00
HoloPanio 4c21245044 add shadow db service for migration drift check 2026-02-25 22:32:40 -06:00
HoloPanio ce456257ea fix prisma migrate diff flag 2026-02-25 22:30:28 -06:00
HoloPanio 8949819396 add CatalogItem migration and CI schema drift check 2026-02-25 22:28:07 -06:00
HoloPanio 49faf97c9b switch to PKCS#8 key format for Bun compatibility 2026-02-25 22:14:19 -06:00
155 changed files with 17877 additions and 162 deletions
+11 -9
View File
@@ -24,9 +24,10 @@ Keep each layer focused:
## Runtime / tooling ## Runtime / tooling
The project runs on **Bun**. DB tooling uses **Prisma**; the generated client lives under `generated/prisma` (do NOT edit generated files). Key scripts in `package.json`: The project runs on **Bun** exclusively — **always use `bun` commands, never `npm`, `npx`, or `yarn`**. DB tooling uses **Prisma**; the generated client lives under `generated/prisma` (do NOT edit generated files). Test preloads are configured in `bunfig.toml` so bare `bun test` works. Key scripts in `package.json`:
- `dev``NODE_ENV=development bun --watch src/index.ts` (start dev server with hot reload) - `dev``NODE_ENV=development bun --watch src/index.ts` (start dev server with hot reload)
- `test``bun test` (runs all tests with preload from `bunfig.toml`)
- `db:gen``prisma generate` - `db:gen``prisma generate`
- `db:push``prisma migrate dev --skip-generate` - `db:push``prisma migrate dev --skip-generate`
- `utils:dev``docker compose -f .docker/docker-compose.yml up --build` - `utils:dev``docker compose -f .docker/docker-compose.yml up --build`
@@ -36,7 +37,7 @@ The project runs on **Bun**. DB tooling uses **Prisma**; the generated client li
## Data layer ## Data layer
Prisma schema is at `prisma/schema.prisma`. The app imports the generated Prisma client from `generated/prisma/client.ts` (or `generated/prisma/browser.ts` for browser type contexts). The shared `prisma` instance is exported from `src/constants.ts`. Always run `npm run db:gen` after updating `schema.prisma`. Prisma schema is at `prisma/schema.prisma`. The app imports the generated Prisma client from `generated/prisma/client.ts` (or `generated/prisma/browser.ts` for browser type contexts). The shared `prisma` instance is exported from `src/constants.ts`. Always run `bun run db:gen` after updating `schema.prisma`.
## Shared constants (`src/constants.ts`) ## Shared constants (`src/constants.ts`)
@@ -174,13 +175,14 @@ The `UnifiClient` class in `src/modules/unifi-api/UnifiClient.ts` wraps all UniF
## Local dev / quick checks ## Local dev / quick checks
- Start dev server: `npm run dev` - Start dev server: `bun run dev`
- Regenerate Prisma client: `npm run db:gen` - Run tests: `bun test`
- Apply DB migrations locally: `npm run db:push` - Regenerate Prisma client: `bun run db:gen`
- Docker dev utilities: `npm run utils:dev` - Apply DB migrations locally: `bun run db:push`
- Generate private keys: `npm run utils:gen_private_keys` - Docker dev utilities: `bun run utils:dev`
- Create admin role: `npm run utils:create_admin_role` - Generate private keys: `bun run utils:gen_private_keys`
- Assign user role: `npm run utils:assign_user_role` - Create admin role: `bun run utils:create_admin_role`
- Assign user role: `bun run utils:assign_user_role`
## When editing generated or infra files ## When editing generated or infra files
+20
View File
@@ -5,8 +5,28 @@ on:
types: [created] types: [created]
jobs: jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Generate Prisma client
run: DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
- name: Run tests
run: bun test --preload ./tests/setup.ts
build: build:
name: Build name: Build
needs: [test]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
+25
View File
@@ -0,0 +1,25 @@
name: Tests
on:
push:
branches: ["**"]
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Generate Prisma client
run: DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
- name: Run tests
run: bun test --preload ./tests/setup.ts
+5
View File
@@ -0,0 +1,5 @@
{
"chat.tools.terminal.autoApprove": {
"bun": true
}
}
+1487
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -67,4 +67,7 @@ RUN bun install --frozen-lockfile
COPY prisma/ prisma/ COPY prisma/ prisma/
COPY prisma.config.ts ./ COPY prisma.config.ts ./
CMD ["bunx", "prisma", "migrate", "deploy"] COPY prisma/migrate-entrypoint.sh ./prisma/migrate-entrypoint.sh
RUN chmod +x prisma/migrate-entrypoint.sh
CMD ["sh", "prisma/migrate-entrypoint.sh"]
+148
View File
@@ -0,0 +1,148 @@
setInternalReview - The quote is ready to be review before it is ready to be sent.
setInternalApproved - The quote has been approved and is ready to be sent out.
setQuoteSent - The Quote has been sent to the customer.
setQuoteConfirmed - The quote has been recieved by the customer.
setRevisionNeeded - The quote needs to be revised and is set to stage revision
setFinalized - This locks any non-admins from modifying the quote saying that is the final iteration of the quote.
convert - This converts the quote to a ticket. It will also update all the necessary fields.
addTime(activityId, user: string)
fetchProducts
updateProduct
addProduct
fetchNotes
addNotes(note: string, user: string)
# Cat/SubCat/Bucket
## Ecosystems vs Categories
## Ecosystem Tree
- Networking
- Manufacturer: Ubiquiti
- Category: Technology
- Subcategory: Network-\*
- Manufacturer: TP-Link
- Category: Technology
- Subcategory: Network-\*
- Video Surveillance
- Manufacturer: Uniview
- Category: Field
- Subcategory: Surveillance-\*
- Manufacturer: Hikvision
- Category: Field
- Subcategory: Surveillance-\*
- Manufacturer: Alarm.com
- Category: Field
- Subcategory: Surveillance-\*
- Burg/Alarm
- Manufacturer: Qolsys
- Category: Field
- Subcategory: AlarmBurg-\*
- DSC
- Category: Field
- Subcategory: AlarmBurg-\*
## Category Tree
- Technology
- GeneralEquip
- Home Entertainment
- Monitor
- Printers
- Storage
- Network
- Network-Other
- Network-Router
- Network-Switch
- Network-Wireless
- Computer
- Computer-Components
- Computer-Desktop
- Computer-Laptop
- Recurring
- Recurring - Online
- Recurring - Other
- Recurring - Protection
- Recurring - Telephone
- Telephone
- Tele-HSet-Digital
- Tele-HSet-IP
- Tele-HSet-SLT
- Tele-Misc
- Tele-Paging
- Tele-SystemCards
- Tele-Systems
- General
- Batteries
- Battery Backups
- BulkWire
- Cables
- Cables-Adapters
- Cables-HDMI
- Cables-Network
- Cables-Other
- Cables-USB
- Cables-VGA
- Elec Cords & Adapters
- Enclosures
- PowerSupply
- RackEquip
- RackEquip-Rack
- RackEquip-Shelves
- Field
- Conduit
- Electric
- GateControl
- Locksets
- Other
- Relays
- AccessControl
- AccessControl-Controllers
- AccessControl-Credential
- AccessControl-LockDevices
- AccessControl-Other
- AccessControl-Readers
- AccessControl-VideoEntry
- AlarmBurg
- AlarmBurg-Communicators
- AlarmBurg-Keypads
- AlarmBurg-Modules
- AlarmBurg-Other
- AlarmBurg-Panels
- AlarmBurg-Sensors
- AlarmBurg-Sensors-Wireless
- AlarmBurg-Sensors-Wired
- AlarmBurg-Siren
- AlarmFire
- AlarmFire-Communicators
- AlarmFire-Devices
- AlarmFire-Modules
- AlarmFire-Other
- AlarmFire-Panels
- AlarmFire-Sensors
- Automation
- Automation-General
- Automation-HVAC
- Automation-Lights
- Automation-Locks
- Automation-Thermostat
- AV
- AV-Adapters&Cables
- AV-Components
- AV-Mounts
- AV-Other
- AV-Speakers
- AV-Television
- StrCbl?
- StrCbl-Jacks
- StrCbl-PatchPanel
- StrCbl-Plates
- Surveillance
- Surveillance-Accs
- Surveillance-CamerasAnalog
- Surveillance-CamerasIP
- Surveillance-NVR
+222
View File
@@ -115,6 +115,57 @@ Admin-specific UI permissions that control visibility and data loading for admin
- **Combine with API permissions**: A user with an admin UI permission should also have the corresponding API permission (e.g., `role.list`) to actually load data. - **Combine with API permissions**: A user with an admin UI permission should also have the corresponding API permission (e.g., `role.list`) to actually load data.
- **Use wildcards for flexibility**: Grant `ui.navigation.*.view` to allow all navigation sections. - **Use wildcards for flexibility**: Grant `ui.navigation.*.view` to allow all navigation sections.
### Procurement Permissions
| Permission Node | Description | Used In | Dependencies |
| --------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
| `procurement.catalog.fetch` | Fetch a single catalog item | [src/api/procurement/[id]/fetch.ts](src/api/procurement/[id]/fetch.ts) | |
| `procurement.catalog.fetch.many` | Fetch multiple catalog items, count, categories/ecosystems, or filter values | [src/api/procurement/fetchAll.ts](src/api/procurement/fetchAll.ts), [src/api/procurement/count.ts](src/api/procurement/count.ts), [src/api/procurement/categories.ts](src/api/procurement/categories.ts), [src/api/procurement/filters.ts](src/api/procurement/filters.ts) | |
| `procurement.catalog.inventory.refresh` | Refresh on-hand inventory for a catalog item from ConnectWise | [src/api/procurement/[id]/refreshInventory.ts](src/api/procurement/[id]/refreshInventory.ts) | `procurement.catalog.fetch` |
| `procurement.catalog.link` | Link or unlink catalog items to each other | [src/api/procurement/[id]/link.ts](src/api/procurement/[id]/link.ts), [src/api/procurement/[id]/unlink.ts](src/api/procurement/[id]/unlink.ts) | `procurement.catalog.fetch` |
### Sales Permissions
Permissions for accessing and managing sales opportunities. Opportunities are synced from ConnectWise and stored locally; sub-resources (products, notes, contacts) are fetched live from CW.
| Permission Node | Description | Used In | Dependencies |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- |
| `sales.opportunity.fetch` | Fetch a single opportunity and its CW sub-resources (products, notes, contacts) | [src/api/sales/[id]/fetch.ts](src/api/sales/[id]/fetch.ts), [src/api/sales/[id]/products.ts](src/api/sales/[id]/products.ts), [src/api/sales/[id]/notes.ts](src/api/sales/[id]/notes.ts), [src/api/sales/[id]/fetchNote.ts](src/api/sales/[id]/fetchNote.ts), [src/api/sales/[id]/contacts.ts](src/api/sales/[id]/contacts.ts) | |
| `sales.opportunity.fetch.many` | Fetch multiple opportunities (paginated/searchable), count, or opportunity types | [src/api/sales/fetchAll.ts](src/api/sales/fetchAll.ts), [src/api/sales/count.ts](src/api/sales/count.ts), [src/api/sales/fetchOpportunityTypes.ts](src/api/sales/fetchOpportunityTypes.ts) | |
| `sales.opportunity.refresh` | Refresh a single opportunity's local data from ConnectWise | [src/api/sales/[id]/refresh.ts](src/api/sales/[id]/refresh.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.note.create` | Create a new note on an opportunity | [src/api/sales/[id]/createNote.ts](src/api/sales/[id]/createNote.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.note.update` | Update an existing note on an opportunity | [src/api/sales/[id]/updateNote.ts](src/api/sales/[id]/updateNote.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.note.delete` | Delete a note from an opportunity | [src/api/sales/[id]/deleteNote.ts](src/api/sales/[id]/deleteNote.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.product.update` | Update products (forecast items) on an opportunity, including resequencing | [src/api/sales/[id]/resequenceProducts.ts](src/api/sales/[id]/resequenceProducts.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.product.add` | Add a new product (forecast item) to an opportunity. Individual fields gated by `sales.opportunity.product.field.<field>` permissions. | [src/api/sales/[id]/addProduct.ts](src/api/sales/[id]/addProduct.ts) | `sales.opportunity.fetch` |
<details>
<summary><strong>Field-level permissions for <code>sales.opportunity.product.add</code></strong></summary>
Each submitted field is gated by a `sales.opportunity.product.field.<field>` permission node. Only fields the user has permission for are forwarded to ConnectWise.
| Field Permission Node | Description |
| ----------------------------------------------------- | -------------------------------------------------------- |
| `sales.opportunity.product.field.catalogItem` | Set the catalog item reference |
| `sales.opportunity.product.field.forecastDescription` | Set the forecast description |
| `sales.opportunity.product.field.productDescription` | Set the product description |
| `sales.opportunity.product.field.quantity` | Set the quantity |
| `sales.opportunity.product.field.status` | Set the status reference |
| `sales.opportunity.product.field.productClass` | Set the product class (e.g. Product, Service, Agreement) |
| `sales.opportunity.product.field.forecastType` | Set the forecast type |
| `sales.opportunity.product.field.revenue` | Set the revenue amount |
| `sales.opportunity.product.field.cost` | Set the cost amount |
| `sales.opportunity.product.field.includeFlag` | Set the include flag |
| `sales.opportunity.product.field.linkFlag` | Set the link flag |
| `sales.opportunity.product.field.recurringFlag` | Set the recurring flag |
| `sales.opportunity.product.field.taxableFlag` | Set the taxable flag |
| `sales.opportunity.product.field.recurringRevenue` | Set the recurring revenue amount |
| `sales.opportunity.product.field.recurringCost` | Set the recurring cost amount |
| `sales.opportunity.product.field.cycles` | Set the number of recurring cycles |
| `sales.opportunity.product.field.sequenceNumber` | Set the sequence number (display order) |
</details>
### UniFi Permissions ### UniFi Permissions
Permissions for accessing and managing UniFi network infrastructure. The `unifi.access` permission is a gate permission required for **all** UniFi routes. Permissions for accessing and managing UniFi network infrastructure. The `unifi.access` permission is a gate permission required for **all** UniFi routes.
@@ -152,6 +203,177 @@ The WiFi fetch route uses `processObjectValuePerms` to filter each WLAN object o
| `unifi.site.wifi.ppsk` | View private pre-shared keys (PPSKs) for a specific WiFi network | [src/api/unifi/site/wifi/ppskFetchAll.ts](src/api/unifi/site/wifi/ppskFetchAll.ts), [src/api/unifi/site/wifi/ppskCreate.ts](src/api/unifi/site/wifi/ppskCreate.ts) | `unifi.access`, `unifi.site.wifi` | | `unifi.site.wifi.ppsk` | View private pre-shared keys (PPSKs) for a specific WiFi network | [src/api/unifi/site/wifi/ppskFetchAll.ts](src/api/unifi/site/wifi/ppskFetchAll.ts), [src/api/unifi/site/wifi/ppskCreate.ts](src/api/unifi/site/wifi/ppskCreate.ts) | `unifi.access`, `unifi.site.wifi` |
| `unifi.site.wifi.ppsk.create` | Create a private pre-shared key on a specific WiFi network | [src/api/unifi/site/wifi/ppskCreate.ts](src/api/unifi/site/wifi/ppskCreate.ts) | `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.ppsk` | | `unifi.site.wifi.ppsk.create` | Create a private pre-shared key on a specific WiFi network | [src/api/unifi/site/wifi/ppskCreate.ts](src/api/unifi/site/wifi/ppskCreate.ts) | `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.ppsk` |
---
## Object Type Permissions (Field-Level Gating)
All fetch and fetchAll routes gate response object keys using `processObjectValuePerms`. For each object type, only fields whose corresponding `<scope>.<field>` permission the user holds are included in the response. Grant `<scope>.*` to allow all fields on that object type.
### Company (`obj.company`)
| Field Permission | Description |
| --------------------------- | ----------------------------------------- |
| `obj.company.id` | View company ID |
| `obj.company.name` | View company name |
| `obj.company.cw_Identifier` | View ConnectWise identifier |
| `obj.company.cw_CompanyId` | View ConnectWise company ID |
| `obj.company.cw_Data` | View ConnectWise data (address, contacts) |
| `obj.company.createdAt` | View creation timestamp |
| `obj.company.updatedAt` | View last-updated timestamp |
**Used in:** [src/api/companies/[id]/fetch.ts](src/api/companies/[id]/fetch.ts), [src/api/companies/fetchAll.ts](src/api/companies/fetchAll.ts)
### Credential (`obj.credential`)
| Field Permission | Description |
| ---------------------------------- | ----------------------------- |
| `obj.credential.id` | View credential ID |
| `obj.credential.name` | View credential name |
| `obj.credential.notes` | View credential notes |
| `obj.credential.typeId` | View credential type ID |
| `obj.credential.companyId` | View linked company ID |
| `obj.credential.subCredentialOfId` | View parent credential ID |
| `obj.credential.fields` | View credential field values |
| `obj.credential.type` | View credential type object |
| `obj.credential.company` | View linked company object |
| `obj.credential.subCredentials` | View sub-credentials array |
| `obj.credential.secureFieldIds` | View secure field identifiers |
| `obj.credential.createdAt` | View creation timestamp |
| `obj.credential.updatedAt` | View last-updated timestamp |
**Used in:** [src/api/credentials/fetch.ts](src/api/credentials/fetch.ts), [src/api/credentials/fetchByCompany.ts](src/api/credentials/fetchByCompany.ts), [src/api/credentials/fetchSubCredentials.ts](src/api/credentials/fetchSubCredentials.ts), [src/api/credential-types/fetchCredentials.ts](src/api/credential-types/fetchCredentials.ts)
### Credential Type (`obj.credentialType`)
| Field Permission | Description |
| ------------------------------------ | ----------------------------------------- |
| `obj.credentialType.id` | View credential type ID |
| `obj.credentialType.name` | View credential type name |
| `obj.credentialType.permissionScope` | View permission scope |
| `obj.credentialType.icon` | View icon |
| `obj.credentialType.fields` | View field definitions |
| `obj.credentialType.credentialCount` | View count of credentials using this type |
| `obj.credentialType.createdAt` | View creation timestamp |
| `obj.credentialType.updatedAt` | View last-updated timestamp |
**Used in:** [src/api/credential-types/fetch.ts](src/api/credential-types/fetch.ts), [src/api/credential-types/fetchAll.ts](src/api/credential-types/fetchAll.ts)
### User (`obj.user`)
| Field Permission | Description |
| ---------------------- | -------------------------------- |
| `obj.user.id` | View user ID |
| `obj.user.name` | View user display name |
| `obj.user.roles` | View assigned role monikers |
| `obj.user.permissions` | View aggregated permission nodes |
| `obj.user.login` | View login identifier |
| `obj.user.email` | View email address |
| `obj.user.image` | View profile image URL |
| `obj.user.createdAt` | View creation timestamp |
| `obj.user.updatedAt` | View last-updated timestamp |
**Used in:** [src/api/user/@me/fetch.ts](src/api/user/@me/fetch.ts), [src/api/user/fetch.ts](src/api/user/fetch.ts), [src/api/user/fetchAll.ts](src/api/user/fetchAll.ts), [src/api/roles/getUsers.ts](src/api/roles/getUsers.ts)
### Role (`obj.role`)
| Field Permission | Description |
| ---------------------- | -------------------------------- |
| `obj.role.id` | View role ID |
| `obj.role.title` | View role title |
| `obj.role.moniker` | View role moniker |
| `obj.role.permissions` | View role permission nodes |
| `obj.role.users` | View users assigned to this role |
| `obj.role.createdAt` | View creation timestamp |
| `obj.role.updatedAt` | View last-updated timestamp |
**Used in:** [src/api/roles/fetch.ts](src/api/roles/fetch.ts), [src/api/roles/fetchAll.ts](src/api/roles/fetchAll.ts), [src/api/user/fetchRoles.ts](src/api/user/fetchRoles.ts)
### Catalog Item (`obj.catalogItem`)
| Field Permission | Description |
| ------------------------------------- | -------------------------------- |
| `obj.catalogItem.id` | View catalog item ID |
| `obj.catalogItem.cwCatalogId` | View ConnectWise catalog ID |
| `obj.catalogItem.identifier` | View item identifier |
| `obj.catalogItem.name` | View item name |
| `obj.catalogItem.description` | View description |
| `obj.catalogItem.customerDescription` | View customer-facing description |
| `obj.catalogItem.internalNotes` | View internal notes |
| `obj.catalogItem.manufacturer` | View manufacturer name |
| `obj.catalogItem.manufactureCwId` | View manufacturer ConnectWise ID |
| `obj.catalogItem.partNumber` | View part number |
| `obj.catalogItem.vendorName` | View vendor name |
| `obj.catalogItem.vendorSku` | View vendor SKU |
| `obj.catalogItem.vendorCwId` | View vendor ConnectWise ID |
| `obj.catalogItem.price` | View price |
| `obj.catalogItem.cost` | View cost |
| `obj.catalogItem.inactive` | View inactive flag |
| `obj.catalogItem.salesTaxable` | View sales-taxable flag |
| `obj.catalogItem.onHand` | View on-hand inventory count |
| `obj.catalogItem.cwLastUpdated` | View CW last-updated timestamp |
| `obj.catalogItem.linkedItems` | View linked catalog items |
| `obj.catalogItem.createdAt` | View creation timestamp |
| `obj.catalogItem.updatedAt` | View last-updated timestamp |
**Used in:** [src/api/procurement/fetchAll.ts](src/api/procurement/fetchAll.ts), [src/api/procurement/[id]/fetch.ts](src/api/procurement/[id]/fetch.ts), [src/api/procurement/[id]/fetchLinked.ts](src/api/procurement/[id]/fetchLinked.ts)
### Opportunity (`obj.opportunity`)
| Field Permission | Description |
| ------------------------------------ | ------------------------------- |
| `obj.opportunity.id` | View opportunity ID |
| `obj.opportunity.cwOpportunityId` | View ConnectWise opportunity ID |
| `obj.opportunity.name` | View opportunity name |
| `obj.opportunity.notes` | View notes |
| `obj.opportunity.type` | View opportunity type |
| `obj.opportunity.stage` | View stage |
| `obj.opportunity.status` | View status |
| `obj.opportunity.priority` | View priority |
| `obj.opportunity.rating` | View rating |
| `obj.opportunity.source` | View source |
| `obj.opportunity.campaign` | View campaign |
| `obj.opportunity.primarySalesRep` | View primary sales rep |
| `obj.opportunity.secondarySalesRep` | View secondary sales rep |
| `obj.opportunity.company` | View company |
| `obj.opportunity.contact` | View contact |
| `obj.opportunity.site` | View site |
| `obj.opportunity.customerPO` | View customer PO |
| `obj.opportunity.totalSalesTax` | View total sales tax |
| `obj.opportunity.location` | View location |
| `obj.opportunity.department` | View department |
| `obj.opportunity.expectedCloseDate` | View expected close date |
| `obj.opportunity.pipelineChangeDate` | View pipeline change date |
| `obj.opportunity.dateBecameLead` | View date became lead |
| `obj.opportunity.closedDate` | View closed date |
| `obj.opportunity.closedFlag` | View closed flag |
| `obj.opportunity.closedBy` | View closed-by member |
| `obj.opportunity.companyId` | View linked company ID |
| `obj.opportunity.cwLastUpdated` | View CW last-updated timestamp |
| `obj.opportunity.createdAt` | View creation timestamp |
| `obj.opportunity.updatedAt` | View last-updated timestamp |
**Used in:** [src/api/sales/fetchAll.ts](src/api/sales/fetchAll.ts), [src/api/sales/[id]/fetch.ts](src/api/sales/[id]/fetch.ts)
### UniFi Site (`obj.unifiSite`)
| Field Permission | Description |
| ------------------------- | ----------------------------- |
| `obj.unifiSite.id` | View site internal ID |
| `obj.unifiSite.name` | View site name |
| `obj.unifiSite.siteId` | View UniFi controller site ID |
| `obj.unifiSite.companyId` | View linked company ID |
| `obj.unifiSite.company` | View linked company object |
| `obj.unifiSite.createdAt` | View creation timestamp |
| `obj.unifiSite.updatedAt` | View last-updated timestamp |
**Used in:** [src/api/unifi/sites/fetchAll.ts](src/api/unifi/sites/fetchAll.ts), [src/api/unifi/site/fetch.ts](src/api/unifi/site/fetch.ts), [src/api/companies/[id]/unifiSites.ts](src/api/companies/[id]/unifiSites.ts)
### WiFi Network (`unifi.site.wifi.read`)
See **UniFi Permissions > Field-Level Permission Gating** above for the full list of `unifi.site.wifi.read.<field>` nodes.
---
## Permission Issuers ## Permission Issuers
Permissions can be issued by different sources: Permissions can be issued by different sources:
+17
View File
@@ -16,6 +16,7 @@
"cors": "^2.8.6", "cors": "^2.8.6",
"cuid": "^3.0.0", "cuid": "^3.0.0",
"hono": "^4.11.5", "hono": "^4.11.5",
"ioredis": "^5.10.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"keypair": "^1.0.4", "keypair": "^1.0.4",
"prisma": "^7.3.0", "prisma": "^7.3.0",
@@ -57,6 +58,8 @@
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
"@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="],
"@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="],
"@prisma/adapter-pg": ["@prisma/adapter-pg@7.3.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.3.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-iuYQMbIPO6i9O45Fv8TB7vWu00BXhCaNAShenqF7gLExGDbnGp5BfFB4yz1K59zQ59jF6tQ9YHrg0P6/J3OoLg=="], "@prisma/adapter-pg": ["@prisma/adapter-pg@7.3.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.3.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-iuYQMbIPO6i9O45Fv8TB7vWu00BXhCaNAShenqF7gLExGDbnGp5BfFB4yz1K59zQ59jF6tQ9YHrg0P6/J3OoLg=="],
@@ -131,6 +134,8 @@
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
@@ -223,6 +228,8 @@
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"ioredis": ["ioredis@5.10.0", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA=="],
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
@@ -241,8 +248,12 @@
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
@@ -331,6 +342,10 @@
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
"regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="], "regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="],
"remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], "remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="],
@@ -363,6 +378,8 @@
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
+2
View File
@@ -0,0 +1,2 @@
[test]
preload = ["./tests/setup.ts"]
+5
View File
@@ -47,6 +47,11 @@ export type Company = Prisma.CompanyModel
* *
*/ */
export type CatalogItem = Prisma.CatalogItemModel export type CatalogItem = Prisma.CatalogItemModel
/**
* Model Opportunity
*
*/
export type Opportunity = Prisma.OpportunityModel
/** /**
* Model CredentialType * Model CredentialType
* *
+5
View File
@@ -69,6 +69,11 @@ export type Company = Prisma.CompanyModel
* *
*/ */
export type CatalogItem = Prisma.CatalogItemModel export type CatalogItem = Prisma.CatalogItemModel
/**
* Model Opportunity
*
*/
export type Opportunity = Prisma.OpportunityModel
/** /**
* Model CredentialType * Model CredentialType
* *
File diff suppressed because one or more lines are too long
+136 -1
View File
@@ -390,6 +390,7 @@ export const ModelName = {
UnifiSite: 'UnifiSite', UnifiSite: 'UnifiSite',
Company: 'Company', Company: 'Company',
CatalogItem: 'CatalogItem', CatalogItem: 'CatalogItem',
Opportunity: 'Opportunity',
CredentialType: 'CredentialType', CredentialType: 'CredentialType',
SecureValue: 'SecureValue', SecureValue: 'SecureValue',
Credential: 'Credential' Credential: 'Credential'
@@ -408,7 +409,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions omit: GlobalOmitOptions
} }
meta: { meta: {
modelProps: "session" | "user" | "role" | "unifiSite" | "company" | "catalogItem" | "credentialType" | "secureValue" | "credential" modelProps: "session" | "user" | "role" | "unifiSite" | "company" | "catalogItem" | "opportunity" | "credentialType" | "secureValue" | "credential"
txIsolationLevel: TransactionIsolationLevel txIsolationLevel: TransactionIsolationLevel
} }
model: { model: {
@@ -856,6 +857,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
} }
} }
} }
Opportunity: {
payload: Prisma.$OpportunityPayload<ExtArgs>
fields: Prisma.OpportunityFieldRefs
operations: {
findUnique: {
args: Prisma.OpportunityFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload> | null
}
findUniqueOrThrow: {
args: Prisma.OpportunityFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
}
findFirst: {
args: Prisma.OpportunityFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload> | null
}
findFirstOrThrow: {
args: Prisma.OpportunityFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
}
findMany: {
args: Prisma.OpportunityFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>[]
}
create: {
args: Prisma.OpportunityCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
}
createMany: {
args: Prisma.OpportunityCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.OpportunityCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>[]
}
delete: {
args: Prisma.OpportunityDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
}
update: {
args: Prisma.OpportunityUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
}
deleteMany: {
args: Prisma.OpportunityDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.OpportunityUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.OpportunityUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>[]
}
upsert: {
args: Prisma.OpportunityUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
}
aggregate: {
args: Prisma.OpportunityAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateOpportunity>
}
groupBy: {
args: Prisma.OpportunityGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.OpportunityGroupByOutputType>[]
}
count: {
args: Prisma.OpportunityCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.OpportunityCountAggregateOutputType> | number
}
}
}
CredentialType: { CredentialType: {
payload: Prisma.$CredentialTypePayload<ExtArgs> payload: Prisma.$CredentialTypePayload<ExtArgs>
fields: Prisma.CredentialTypeFieldRefs fields: Prisma.CredentialTypeFieldRefs
@@ -1138,6 +1213,7 @@ export const UserScalarFieldEnum = {
email: 'email', email: 'email',
emailVerified: 'emailVerified', emailVerified: 'emailVerified',
image: 'image', image: 'image',
cwIdentifier: 'cwIdentifier',
userId: 'userId', userId: 'userId',
token: 'token', token: 'token',
createdAt: 'createdAt', createdAt: 'createdAt',
@@ -1186,10 +1262,15 @@ export type CompanyScalarFieldEnum = (typeof CompanyScalarFieldEnum)[keyof typeo
export const CatalogItemScalarFieldEnum = { export const CatalogItemScalarFieldEnum = {
id: 'id', id: 'id',
cwCatalogId: 'cwCatalogId', cwCatalogId: 'cwCatalogId',
identifier: 'identifier',
name: 'name', name: 'name',
description: 'description', description: 'description',
customerDescription: 'customerDescription', customerDescription: 'customerDescription',
internalNotes: 'internalNotes', internalNotes: 'internalNotes',
category: 'category',
categoryCwId: 'categoryCwId',
subcategory: 'subcategory',
subcategoryCwId: 'subcategoryCwId',
manufacturer: 'manufacturer', manufacturer: 'manufacturer',
manufactureCwId: 'manufactureCwId', manufactureCwId: 'manufactureCwId',
partNumber: 'partNumber', partNumber: 'partNumber',
@@ -1209,6 +1290,59 @@ export const CatalogItemScalarFieldEnum = {
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum] export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
export const OpportunityScalarFieldEnum = {
id: 'id',
cwOpportunityId: 'cwOpportunityId',
name: 'name',
notes: 'notes',
typeName: 'typeName',
typeCwId: 'typeCwId',
stageName: 'stageName',
stageCwId: 'stageCwId',
statusName: 'statusName',
statusCwId: 'statusCwId',
priorityName: 'priorityName',
priorityCwId: 'priorityCwId',
ratingName: 'ratingName',
ratingCwId: 'ratingCwId',
source: 'source',
campaignName: 'campaignName',
campaignCwId: 'campaignCwId',
primarySalesRepName: 'primarySalesRepName',
primarySalesRepIdentifier: 'primarySalesRepIdentifier',
primarySalesRepCwId: 'primarySalesRepCwId',
secondarySalesRepName: 'secondarySalesRepName',
secondarySalesRepIdentifier: 'secondarySalesRepIdentifier',
secondarySalesRepCwId: 'secondarySalesRepCwId',
companyCwId: 'companyCwId',
companyName: 'companyName',
contactCwId: 'contactCwId',
contactName: 'contactName',
siteCwId: 'siteCwId',
siteName: 'siteName',
customerPO: 'customerPO',
totalSalesTax: 'totalSalesTax',
locationName: 'locationName',
locationCwId: 'locationCwId',
departmentName: 'departmentName',
departmentCwId: 'departmentCwId',
expectedCloseDate: 'expectedCloseDate',
pipelineChangeDate: 'pipelineChangeDate',
dateBecameLead: 'dateBecameLead',
closedDate: 'closedDate',
closedFlag: 'closedFlag',
closedByName: 'closedByName',
closedByCwId: 'closedByCwId',
companyId: 'companyId',
productSequence: 'productSequence',
cwLastUpdated: 'cwLastUpdated',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type OpportunityScalarFieldEnum = (typeof OpportunityScalarFieldEnum)[keyof typeof OpportunityScalarFieldEnum]
export const CredentialTypeScalarFieldEnum = { export const CredentialTypeScalarFieldEnum = {
id: 'id', id: 'id',
name: 'name', name: 'name',
@@ -1473,6 +1607,7 @@ export type GlobalOmitConfig = {
unifiSite?: Prisma.UnifiSiteOmit unifiSite?: Prisma.UnifiSiteOmit
company?: Prisma.CompanyOmit company?: Prisma.CompanyOmit
catalogItem?: Prisma.CatalogItemOmit catalogItem?: Prisma.CatalogItemOmit
opportunity?: Prisma.OpportunityOmit
credentialType?: Prisma.CredentialTypeOmit credentialType?: Prisma.CredentialTypeOmit
secureValue?: Prisma.SecureValueOmit secureValue?: Prisma.SecureValueOmit
credential?: Prisma.CredentialOmit credential?: Prisma.CredentialOmit
@@ -57,6 +57,7 @@ export const ModelName = {
UnifiSite: 'UnifiSite', UnifiSite: 'UnifiSite',
Company: 'Company', Company: 'Company',
CatalogItem: 'CatalogItem', CatalogItem: 'CatalogItem',
Opportunity: 'Opportunity',
CredentialType: 'CredentialType', CredentialType: 'CredentialType',
SecureValue: 'SecureValue', SecureValue: 'SecureValue',
Credential: 'Credential' Credential: 'Credential'
@@ -99,6 +100,7 @@ export const UserScalarFieldEnum = {
email: 'email', email: 'email',
emailVerified: 'emailVerified', emailVerified: 'emailVerified',
image: 'image', image: 'image',
cwIdentifier: 'cwIdentifier',
userId: 'userId', userId: 'userId',
token: 'token', token: 'token',
createdAt: 'createdAt', createdAt: 'createdAt',
@@ -147,10 +149,15 @@ export type CompanyScalarFieldEnum = (typeof CompanyScalarFieldEnum)[keyof typeo
export const CatalogItemScalarFieldEnum = { export const CatalogItemScalarFieldEnum = {
id: 'id', id: 'id',
cwCatalogId: 'cwCatalogId', cwCatalogId: 'cwCatalogId',
identifier: 'identifier',
name: 'name', name: 'name',
description: 'description', description: 'description',
customerDescription: 'customerDescription', customerDescription: 'customerDescription',
internalNotes: 'internalNotes', internalNotes: 'internalNotes',
category: 'category',
categoryCwId: 'categoryCwId',
subcategory: 'subcategory',
subcategoryCwId: 'subcategoryCwId',
manufacturer: 'manufacturer', manufacturer: 'manufacturer',
manufactureCwId: 'manufactureCwId', manufactureCwId: 'manufactureCwId',
partNumber: 'partNumber', partNumber: 'partNumber',
@@ -170,6 +177,59 @@ export const CatalogItemScalarFieldEnum = {
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum] export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
export const OpportunityScalarFieldEnum = {
id: 'id',
cwOpportunityId: 'cwOpportunityId',
name: 'name',
notes: 'notes',
typeName: 'typeName',
typeCwId: 'typeCwId',
stageName: 'stageName',
stageCwId: 'stageCwId',
statusName: 'statusName',
statusCwId: 'statusCwId',
priorityName: 'priorityName',
priorityCwId: 'priorityCwId',
ratingName: 'ratingName',
ratingCwId: 'ratingCwId',
source: 'source',
campaignName: 'campaignName',
campaignCwId: 'campaignCwId',
primarySalesRepName: 'primarySalesRepName',
primarySalesRepIdentifier: 'primarySalesRepIdentifier',
primarySalesRepCwId: 'primarySalesRepCwId',
secondarySalesRepName: 'secondarySalesRepName',
secondarySalesRepIdentifier: 'secondarySalesRepIdentifier',
secondarySalesRepCwId: 'secondarySalesRepCwId',
companyCwId: 'companyCwId',
companyName: 'companyName',
contactCwId: 'contactCwId',
contactName: 'contactName',
siteCwId: 'siteCwId',
siteName: 'siteName',
customerPO: 'customerPO',
totalSalesTax: 'totalSalesTax',
locationName: 'locationName',
locationCwId: 'locationCwId',
departmentName: 'departmentName',
departmentCwId: 'departmentCwId',
expectedCloseDate: 'expectedCloseDate',
pipelineChangeDate: 'pipelineChangeDate',
dateBecameLead: 'dateBecameLead',
closedDate: 'closedDate',
closedFlag: 'closedFlag',
closedByName: 'closedByName',
closedByCwId: 'closedByCwId',
companyId: 'companyId',
productSequence: 'productSequence',
cwLastUpdated: 'cwLastUpdated',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type OpportunityScalarFieldEnum = (typeof OpportunityScalarFieldEnum)[keyof typeof OpportunityScalarFieldEnum]
export const CredentialTypeScalarFieldEnum = { export const CredentialTypeScalarFieldEnum = {
id: 'id', id: 'id',
name: 'name', name: 'name',
+1
View File
@@ -14,6 +14,7 @@ export type * from './models/Role.ts'
export type * from './models/UnifiSite.ts' export type * from './models/UnifiSite.ts'
export type * from './models/Company.ts' export type * from './models/Company.ts'
export type * from './models/CatalogItem.ts' export type * from './models/CatalogItem.ts'
export type * from './models/Opportunity.ts'
export type * from './models/CredentialType.ts' export type * from './models/CredentialType.ts'
export type * from './models/SecureValue.ts' export type * from './models/SecureValue.ts'
export type * from './models/Credential.ts' export type * from './models/Credential.ts'
+209 -2
View File
@@ -28,6 +28,8 @@ export type AggregateCatalogItem = {
export type CatalogItemAvgAggregateOutputType = { export type CatalogItemAvgAggregateOutputType = {
cwCatalogId: number | null cwCatalogId: number | null
categoryCwId: number | null
subcategoryCwId: number | null
manufactureCwId: number | null manufactureCwId: number | null
vendorCwId: number | null vendorCwId: number | null
price: number | null price: number | null
@@ -37,6 +39,8 @@ export type CatalogItemAvgAggregateOutputType = {
export type CatalogItemSumAggregateOutputType = { export type CatalogItemSumAggregateOutputType = {
cwCatalogId: number | null cwCatalogId: number | null
categoryCwId: number | null
subcategoryCwId: number | null
manufactureCwId: number | null manufactureCwId: number | null
vendorCwId: number | null vendorCwId: number | null
price: number | null price: number | null
@@ -47,10 +51,15 @@ export type CatalogItemSumAggregateOutputType = {
export type CatalogItemMinAggregateOutputType = { export type CatalogItemMinAggregateOutputType = {
id: string | null id: string | null
cwCatalogId: number | null cwCatalogId: number | null
identifier: string | null
name: string | null name: string | null
description: string | null description: string | null
customerDescription: string | null customerDescription: string | null
internalNotes: string | null internalNotes: string | null
category: string | null
categoryCwId: number | null
subcategory: string | null
subcategoryCwId: number | null
manufacturer: string | null manufacturer: string | null
manufactureCwId: number | null manufactureCwId: number | null
partNumber: string | null partNumber: string | null
@@ -70,10 +79,15 @@ export type CatalogItemMinAggregateOutputType = {
export type CatalogItemMaxAggregateOutputType = { export type CatalogItemMaxAggregateOutputType = {
id: string | null id: string | null
cwCatalogId: number | null cwCatalogId: number | null
identifier: string | null
name: string | null name: string | null
description: string | null description: string | null
customerDescription: string | null customerDescription: string | null
internalNotes: string | null internalNotes: string | null
category: string | null
categoryCwId: number | null
subcategory: string | null
subcategoryCwId: number | null
manufacturer: string | null manufacturer: string | null
manufactureCwId: number | null manufactureCwId: number | null
partNumber: string | null partNumber: string | null
@@ -93,10 +107,15 @@ export type CatalogItemMaxAggregateOutputType = {
export type CatalogItemCountAggregateOutputType = { export type CatalogItemCountAggregateOutputType = {
id: number id: number
cwCatalogId: number cwCatalogId: number
identifier: number
name: number name: number
description: number description: number
customerDescription: number customerDescription: number
internalNotes: number internalNotes: number
category: number
categoryCwId: number
subcategory: number
subcategoryCwId: number
manufacturer: number manufacturer: number
manufactureCwId: number manufactureCwId: number
partNumber: number partNumber: number
@@ -117,6 +136,8 @@ export type CatalogItemCountAggregateOutputType = {
export type CatalogItemAvgAggregateInputType = { export type CatalogItemAvgAggregateInputType = {
cwCatalogId?: true cwCatalogId?: true
categoryCwId?: true
subcategoryCwId?: true
manufactureCwId?: true manufactureCwId?: true
vendorCwId?: true vendorCwId?: true
price?: true price?: true
@@ -126,6 +147,8 @@ export type CatalogItemAvgAggregateInputType = {
export type CatalogItemSumAggregateInputType = { export type CatalogItemSumAggregateInputType = {
cwCatalogId?: true cwCatalogId?: true
categoryCwId?: true
subcategoryCwId?: true
manufactureCwId?: true manufactureCwId?: true
vendorCwId?: true vendorCwId?: true
price?: true price?: true
@@ -136,10 +159,15 @@ export type CatalogItemSumAggregateInputType = {
export type CatalogItemMinAggregateInputType = { export type CatalogItemMinAggregateInputType = {
id?: true id?: true
cwCatalogId?: true cwCatalogId?: true
identifier?: true
name?: true name?: true
description?: true description?: true
customerDescription?: true customerDescription?: true
internalNotes?: true internalNotes?: true
category?: true
categoryCwId?: true
subcategory?: true
subcategoryCwId?: true
manufacturer?: true manufacturer?: true
manufactureCwId?: true manufactureCwId?: true
partNumber?: true partNumber?: true
@@ -159,10 +187,15 @@ export type CatalogItemMinAggregateInputType = {
export type CatalogItemMaxAggregateInputType = { export type CatalogItemMaxAggregateInputType = {
id?: true id?: true
cwCatalogId?: true cwCatalogId?: true
identifier?: true
name?: true name?: true
description?: true description?: true
customerDescription?: true customerDescription?: true
internalNotes?: true internalNotes?: true
category?: true
categoryCwId?: true
subcategory?: true
subcategoryCwId?: true
manufacturer?: true manufacturer?: true
manufactureCwId?: true manufactureCwId?: true
partNumber?: true partNumber?: true
@@ -182,10 +215,15 @@ export type CatalogItemMaxAggregateInputType = {
export type CatalogItemCountAggregateInputType = { export type CatalogItemCountAggregateInputType = {
id?: true id?: true
cwCatalogId?: true cwCatalogId?: true
identifier?: true
name?: true name?: true
description?: true description?: true
customerDescription?: true customerDescription?: true
internalNotes?: true internalNotes?: true
category?: true
categoryCwId?: true
subcategory?: true
subcategoryCwId?: true
manufacturer?: true manufacturer?: true
manufactureCwId?: true manufactureCwId?: true
partNumber?: true partNumber?: true
@@ -292,10 +330,15 @@ export type CatalogItemGroupByArgs<ExtArgs extends runtime.Types.Extensions.Inte
export type CatalogItemGroupByOutputType = { export type CatalogItemGroupByOutputType = {
id: string id: string
cwCatalogId: number cwCatalogId: number
identifier: string | null
name: string name: string
description: string | null description: string | null
customerDescription: string | null customerDescription: string | null
internalNotes: string | null internalNotes: string | null
category: string | null
categoryCwId: number | null
subcategory: string | null
subcategoryCwId: number | null
manufacturer: string | null manufacturer: string | null
manufactureCwId: number | null manufactureCwId: number | null
partNumber: string | null partNumber: string | null
@@ -338,10 +381,15 @@ export type CatalogItemWhereInput = {
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[] NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
id?: Prisma.StringFilter<"CatalogItem"> | string id?: Prisma.StringFilter<"CatalogItem"> | string
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
identifier?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
name?: Prisma.StringFilter<"CatalogItem"> | string name?: Prisma.StringFilter<"CatalogItem"> | string
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
category?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
categoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
subcategory?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
subcategoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
@@ -363,10 +411,15 @@ export type CatalogItemWhereInput = {
export type CatalogItemOrderByWithRelationInput = { export type CatalogItemOrderByWithRelationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
cwCatalogId?: Prisma.SortOrder cwCatalogId?: Prisma.SortOrder
identifier?: Prisma.SortOrderInput | Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
description?: Prisma.SortOrderInput | Prisma.SortOrder description?: Prisma.SortOrderInput | Prisma.SortOrder
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
internalNotes?: Prisma.SortOrderInput | Prisma.SortOrder internalNotes?: Prisma.SortOrderInput | Prisma.SortOrder
category?: Prisma.SortOrderInput | Prisma.SortOrder
categoryCwId?: Prisma.SortOrderInput | Prisma.SortOrder
subcategory?: Prisma.SortOrderInput | Prisma.SortOrder
subcategoryCwId?: Prisma.SortOrderInput | Prisma.SortOrder
manufacturer?: Prisma.SortOrderInput | Prisma.SortOrder manufacturer?: Prisma.SortOrderInput | Prisma.SortOrder
manufactureCwId?: Prisma.SortOrderInput | Prisma.SortOrder manufactureCwId?: Prisma.SortOrderInput | Prisma.SortOrder
partNumber?: Prisma.SortOrderInput | Prisma.SortOrder partNumber?: Prisma.SortOrderInput | Prisma.SortOrder
@@ -388,6 +441,7 @@ export type CatalogItemOrderByWithRelationInput = {
export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{ export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
id?: string id?: string
cwCatalogId?: number cwCatalogId?: number
identifier?: string
AND?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[] AND?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
OR?: Prisma.CatalogItemWhereInput[] OR?: Prisma.CatalogItemWhereInput[]
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[] NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
@@ -395,6 +449,10 @@ export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
category?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
categoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
subcategory?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
subcategoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
@@ -411,15 +469,20 @@ export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
updatedAt?: Prisma.DateTimeFilter<"CatalogItem"> | Date | string updatedAt?: Prisma.DateTimeFilter<"CatalogItem"> | Date | string
linkedItems?: Prisma.CatalogItemListRelationFilter linkedItems?: Prisma.CatalogItemListRelationFilter
linkedTo?: Prisma.CatalogItemListRelationFilter linkedTo?: Prisma.CatalogItemListRelationFilter
}, "id" | "cwCatalogId"> }, "id" | "cwCatalogId" | "identifier">
export type CatalogItemOrderByWithAggregationInput = { export type CatalogItemOrderByWithAggregationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
cwCatalogId?: Prisma.SortOrder cwCatalogId?: Prisma.SortOrder
identifier?: Prisma.SortOrderInput | Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
description?: Prisma.SortOrderInput | Prisma.SortOrder description?: Prisma.SortOrderInput | Prisma.SortOrder
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
internalNotes?: Prisma.SortOrderInput | Prisma.SortOrder internalNotes?: Prisma.SortOrderInput | Prisma.SortOrder
category?: Prisma.SortOrderInput | Prisma.SortOrder
categoryCwId?: Prisma.SortOrderInput | Prisma.SortOrder
subcategory?: Prisma.SortOrderInput | Prisma.SortOrder
subcategoryCwId?: Prisma.SortOrderInput | Prisma.SortOrder
manufacturer?: Prisma.SortOrderInput | Prisma.SortOrder manufacturer?: Prisma.SortOrderInput | Prisma.SortOrder
manufactureCwId?: Prisma.SortOrderInput | Prisma.SortOrder manufactureCwId?: Prisma.SortOrderInput | Prisma.SortOrder
partNumber?: Prisma.SortOrderInput | Prisma.SortOrder partNumber?: Prisma.SortOrderInput | Prisma.SortOrder
@@ -447,10 +510,15 @@ export type CatalogItemScalarWhereWithAggregatesInput = {
NOT?: Prisma.CatalogItemScalarWhereWithAggregatesInput | Prisma.CatalogItemScalarWhereWithAggregatesInput[] NOT?: Prisma.CatalogItemScalarWhereWithAggregatesInput | Prisma.CatalogItemScalarWhereWithAggregatesInput[]
id?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string id?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
cwCatalogId?: Prisma.IntWithAggregatesFilter<"CatalogItem"> | number cwCatalogId?: Prisma.IntWithAggregatesFilter<"CatalogItem"> | number
identifier?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
name?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string name?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
description?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null description?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
customerDescription?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null customerDescription?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
internalNotes?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null internalNotes?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
category?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
categoryCwId?: Prisma.IntNullableWithAggregatesFilter<"CatalogItem"> | number | null
subcategory?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
subcategoryCwId?: Prisma.IntNullableWithAggregatesFilter<"CatalogItem"> | number | null
manufacturer?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null manufacturer?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
manufactureCwId?: Prisma.IntNullableWithAggregatesFilter<"CatalogItem"> | number | null manufactureCwId?: Prisma.IntNullableWithAggregatesFilter<"CatalogItem"> | number | null
partNumber?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null partNumber?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
@@ -470,10 +538,15 @@ export type CatalogItemScalarWhereWithAggregatesInput = {
export type CatalogItemCreateInput = { export type CatalogItemCreateInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
internalNotes?: string | null internalNotes?: string | null
category?: string | null
categoryCwId?: number | null
subcategory?: string | null
subcategoryCwId?: number | null
manufacturer?: string | null manufacturer?: string | null
manufactureCwId?: number | null manufactureCwId?: number | null
partNumber?: string | null partNumber?: string | null
@@ -495,10 +568,15 @@ export type CatalogItemCreateInput = {
export type CatalogItemUncheckedCreateInput = { export type CatalogItemUncheckedCreateInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
internalNotes?: string | null internalNotes?: string | null
category?: string | null
categoryCwId?: number | null
subcategory?: string | null
subcategoryCwId?: number | null
manufacturer?: string | null manufacturer?: string | null
manufactureCwId?: number | null manufactureCwId?: number | null
partNumber?: string | null partNumber?: string | null
@@ -520,10 +598,15 @@ export type CatalogItemUncheckedCreateInput = {
export type CatalogItemUpdateInput = { export type CatalogItemUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -545,10 +628,15 @@ export type CatalogItemUpdateInput = {
export type CatalogItemUncheckedUpdateInput = { export type CatalogItemUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -570,10 +658,15 @@ export type CatalogItemUncheckedUpdateInput = {
export type CatalogItemCreateManyInput = { export type CatalogItemCreateManyInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
internalNotes?: string | null internalNotes?: string | null
category?: string | null
categoryCwId?: number | null
subcategory?: string | null
subcategoryCwId?: number | null
manufacturer?: string | null manufacturer?: string | null
manufactureCwId?: number | null manufactureCwId?: number | null
partNumber?: string | null partNumber?: string | null
@@ -593,10 +686,15 @@ export type CatalogItemCreateManyInput = {
export type CatalogItemUpdateManyMutationInput = { export type CatalogItemUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -616,10 +714,15 @@ export type CatalogItemUpdateManyMutationInput = {
export type CatalogItemUncheckedUpdateManyInput = { export type CatalogItemUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -649,10 +752,15 @@ export type CatalogItemOrderByRelationAggregateInput = {
export type CatalogItemCountOrderByAggregateInput = { export type CatalogItemCountOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
cwCatalogId?: Prisma.SortOrder cwCatalogId?: Prisma.SortOrder
identifier?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
description?: Prisma.SortOrder description?: Prisma.SortOrder
customerDescription?: Prisma.SortOrder customerDescription?: Prisma.SortOrder
internalNotes?: Prisma.SortOrder internalNotes?: Prisma.SortOrder
category?: Prisma.SortOrder
categoryCwId?: Prisma.SortOrder
subcategory?: Prisma.SortOrder
subcategoryCwId?: Prisma.SortOrder
manufacturer?: Prisma.SortOrder manufacturer?: Prisma.SortOrder
manufactureCwId?: Prisma.SortOrder manufactureCwId?: Prisma.SortOrder
partNumber?: Prisma.SortOrder partNumber?: Prisma.SortOrder
@@ -671,6 +779,8 @@ export type CatalogItemCountOrderByAggregateInput = {
export type CatalogItemAvgOrderByAggregateInput = { export type CatalogItemAvgOrderByAggregateInput = {
cwCatalogId?: Prisma.SortOrder cwCatalogId?: Prisma.SortOrder
categoryCwId?: Prisma.SortOrder
subcategoryCwId?: Prisma.SortOrder
manufactureCwId?: Prisma.SortOrder manufactureCwId?: Prisma.SortOrder
vendorCwId?: Prisma.SortOrder vendorCwId?: Prisma.SortOrder
price?: Prisma.SortOrder price?: Prisma.SortOrder
@@ -681,10 +791,15 @@ export type CatalogItemAvgOrderByAggregateInput = {
export type CatalogItemMaxOrderByAggregateInput = { export type CatalogItemMaxOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
cwCatalogId?: Prisma.SortOrder cwCatalogId?: Prisma.SortOrder
identifier?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
description?: Prisma.SortOrder description?: Prisma.SortOrder
customerDescription?: Prisma.SortOrder customerDescription?: Prisma.SortOrder
internalNotes?: Prisma.SortOrder internalNotes?: Prisma.SortOrder
category?: Prisma.SortOrder
categoryCwId?: Prisma.SortOrder
subcategory?: Prisma.SortOrder
subcategoryCwId?: Prisma.SortOrder
manufacturer?: Prisma.SortOrder manufacturer?: Prisma.SortOrder
manufactureCwId?: Prisma.SortOrder manufactureCwId?: Prisma.SortOrder
partNumber?: Prisma.SortOrder partNumber?: Prisma.SortOrder
@@ -704,10 +819,15 @@ export type CatalogItemMaxOrderByAggregateInput = {
export type CatalogItemMinOrderByAggregateInput = { export type CatalogItemMinOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
cwCatalogId?: Prisma.SortOrder cwCatalogId?: Prisma.SortOrder
identifier?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
description?: Prisma.SortOrder description?: Prisma.SortOrder
customerDescription?: Prisma.SortOrder customerDescription?: Prisma.SortOrder
internalNotes?: Prisma.SortOrder internalNotes?: Prisma.SortOrder
category?: Prisma.SortOrder
categoryCwId?: Prisma.SortOrder
subcategory?: Prisma.SortOrder
subcategoryCwId?: Prisma.SortOrder
manufacturer?: Prisma.SortOrder manufacturer?: Prisma.SortOrder
manufactureCwId?: Prisma.SortOrder manufactureCwId?: Prisma.SortOrder
partNumber?: Prisma.SortOrder partNumber?: Prisma.SortOrder
@@ -726,6 +846,8 @@ export type CatalogItemMinOrderByAggregateInput = {
export type CatalogItemSumOrderByAggregateInput = { export type CatalogItemSumOrderByAggregateInput = {
cwCatalogId?: Prisma.SortOrder cwCatalogId?: Prisma.SortOrder
categoryCwId?: Prisma.SortOrder
subcategoryCwId?: Prisma.SortOrder
manufactureCwId?: Prisma.SortOrder manufactureCwId?: Prisma.SortOrder
vendorCwId?: Prisma.SortOrder vendorCwId?: Prisma.SortOrder
price?: Prisma.SortOrder price?: Prisma.SortOrder
@@ -828,10 +950,15 @@ export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsNestedInput = {
export type CatalogItemCreateWithoutLinkedToInput = { export type CatalogItemCreateWithoutLinkedToInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
internalNotes?: string | null internalNotes?: string | null
category?: string | null
categoryCwId?: number | null
subcategory?: string | null
subcategoryCwId?: number | null
manufacturer?: string | null manufacturer?: string | null
manufactureCwId?: number | null manufactureCwId?: number | null
partNumber?: string | null partNumber?: string | null
@@ -852,10 +979,15 @@ export type CatalogItemCreateWithoutLinkedToInput = {
export type CatalogItemUncheckedCreateWithoutLinkedToInput = { export type CatalogItemUncheckedCreateWithoutLinkedToInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
internalNotes?: string | null internalNotes?: string | null
category?: string | null
categoryCwId?: number | null
subcategory?: string | null
subcategoryCwId?: number | null
manufacturer?: string | null manufacturer?: string | null
manufactureCwId?: number | null manufactureCwId?: number | null
partNumber?: string | null partNumber?: string | null
@@ -881,10 +1013,15 @@ export type CatalogItemCreateOrConnectWithoutLinkedToInput = {
export type CatalogItemCreateWithoutLinkedItemsInput = { export type CatalogItemCreateWithoutLinkedItemsInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
internalNotes?: string | null internalNotes?: string | null
category?: string | null
categoryCwId?: number | null
subcategory?: string | null
subcategoryCwId?: number | null
manufacturer?: string | null manufacturer?: string | null
manufactureCwId?: number | null manufactureCwId?: number | null
partNumber?: string | null partNumber?: string | null
@@ -905,10 +1042,15 @@ export type CatalogItemCreateWithoutLinkedItemsInput = {
export type CatalogItemUncheckedCreateWithoutLinkedItemsInput = { export type CatalogItemUncheckedCreateWithoutLinkedItemsInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
internalNotes?: string | null internalNotes?: string | null
category?: string | null
categoryCwId?: number | null
subcategory?: string | null
subcategoryCwId?: number | null
manufacturer?: string | null manufacturer?: string | null
manufactureCwId?: number | null manufactureCwId?: number | null
partNumber?: string | null partNumber?: string | null
@@ -953,10 +1095,15 @@ export type CatalogItemScalarWhereInput = {
NOT?: Prisma.CatalogItemScalarWhereInput | Prisma.CatalogItemScalarWhereInput[] NOT?: Prisma.CatalogItemScalarWhereInput | Prisma.CatalogItemScalarWhereInput[]
id?: Prisma.StringFilter<"CatalogItem"> | string id?: Prisma.StringFilter<"CatalogItem"> | string
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
identifier?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
name?: Prisma.StringFilter<"CatalogItem"> | string name?: Prisma.StringFilter<"CatalogItem"> | string
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null internalNotes?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
category?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
categoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
subcategory?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
subcategoryCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null manufacturer?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null manufactureCwId?: Prisma.IntNullableFilter<"CatalogItem"> | number | null
partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null partNumber?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
@@ -992,10 +1139,15 @@ export type CatalogItemUpdateManyWithWhereWithoutLinkedItemsInput = {
export type CatalogItemUpdateWithoutLinkedToInput = { export type CatalogItemUpdateWithoutLinkedToInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -1016,10 +1168,15 @@ export type CatalogItemUpdateWithoutLinkedToInput = {
export type CatalogItemUncheckedUpdateWithoutLinkedToInput = { export type CatalogItemUncheckedUpdateWithoutLinkedToInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -1040,10 +1197,15 @@ export type CatalogItemUncheckedUpdateWithoutLinkedToInput = {
export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = { export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -1063,10 +1225,15 @@ export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = {
export type CatalogItemUpdateWithoutLinkedItemsInput = { export type CatalogItemUpdateWithoutLinkedItemsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -1087,10 +1254,15 @@ export type CatalogItemUpdateWithoutLinkedItemsInput = {
export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = { export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -1111,10 +1283,15 @@ export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = {
export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsInput = { export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null internalNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
categoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
subcategory?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
subcategoryCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null manufacturer?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null manufactureCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null partNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -1174,10 +1351,15 @@ export type CatalogItemCountOutputTypeCountLinkedToArgs<ExtArgs extends runtime.
export type CatalogItemSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type CatalogItemSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
cwCatalogId?: boolean cwCatalogId?: boolean
identifier?: boolean
name?: boolean name?: boolean
description?: boolean description?: boolean
customerDescription?: boolean customerDescription?: boolean
internalNotes?: boolean internalNotes?: boolean
category?: boolean
categoryCwId?: boolean
subcategory?: boolean
subcategoryCwId?: boolean
manufacturer?: boolean manufacturer?: boolean
manufactureCwId?: boolean manufactureCwId?: boolean
partNumber?: boolean partNumber?: boolean
@@ -1200,10 +1382,15 @@ export type CatalogItemSelect<ExtArgs extends runtime.Types.Extensions.InternalA
export type CatalogItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type CatalogItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
cwCatalogId?: boolean cwCatalogId?: boolean
identifier?: boolean
name?: boolean name?: boolean
description?: boolean description?: boolean
customerDescription?: boolean customerDescription?: boolean
internalNotes?: boolean internalNotes?: boolean
category?: boolean
categoryCwId?: boolean
subcategory?: boolean
subcategoryCwId?: boolean
manufacturer?: boolean manufacturer?: boolean
manufactureCwId?: boolean manufactureCwId?: boolean
partNumber?: boolean partNumber?: boolean
@@ -1223,10 +1410,15 @@ export type CatalogItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.E
export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
cwCatalogId?: boolean cwCatalogId?: boolean
identifier?: boolean
name?: boolean name?: boolean
description?: boolean description?: boolean
customerDescription?: boolean customerDescription?: boolean
internalNotes?: boolean internalNotes?: boolean
category?: boolean
categoryCwId?: boolean
subcategory?: boolean
subcategoryCwId?: boolean
manufacturer?: boolean manufacturer?: boolean
manufactureCwId?: boolean manufactureCwId?: boolean
partNumber?: boolean partNumber?: boolean
@@ -1246,10 +1438,15 @@ export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.E
export type CatalogItemSelectScalar = { export type CatalogItemSelectScalar = {
id?: boolean id?: boolean
cwCatalogId?: boolean cwCatalogId?: boolean
identifier?: boolean
name?: boolean name?: boolean
description?: boolean description?: boolean
customerDescription?: boolean customerDescription?: boolean
internalNotes?: boolean internalNotes?: boolean
category?: boolean
categoryCwId?: boolean
subcategory?: boolean
subcategoryCwId?: boolean
manufacturer?: boolean manufacturer?: boolean
manufactureCwId?: boolean manufactureCwId?: boolean
partNumber?: boolean partNumber?: boolean
@@ -1266,7 +1463,7 @@ export type CatalogItemSelectScalar = {
updatedAt?: boolean updatedAt?: boolean
} }
export type CatalogItemOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "cwCatalogId" | "name" | "description" | "customerDescription" | "internalNotes" | "manufacturer" | "manufactureCwId" | "partNumber" | "vendorName" | "vendorSku" | "vendorCwId" | "price" | "cost" | "inactive" | "salesTaxable" | "onHand" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["catalogItem"]> export type CatalogItemOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "cwCatalogId" | "identifier" | "name" | "description" | "customerDescription" | "internalNotes" | "category" | "categoryCwId" | "subcategory" | "subcategoryCwId" | "manufacturer" | "manufactureCwId" | "partNumber" | "vendorName" | "vendorSku" | "vendorCwId" | "price" | "cost" | "inactive" | "salesTaxable" | "onHand" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["catalogItem"]>
export type CatalogItemInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type CatalogItemInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
linkedItems?: boolean | Prisma.CatalogItem$linkedItemsArgs<ExtArgs> linkedItems?: boolean | Prisma.CatalogItem$linkedItemsArgs<ExtArgs>
linkedTo?: boolean | Prisma.CatalogItem$linkedToArgs<ExtArgs> linkedTo?: boolean | Prisma.CatalogItem$linkedToArgs<ExtArgs>
@@ -1284,10 +1481,15 @@ export type $CatalogItemPayload<ExtArgs extends runtime.Types.Extensions.Interna
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string id: string
cwCatalogId: number cwCatalogId: number
identifier: string | null
name: string name: string
description: string | null description: string | null
customerDescription: string | null customerDescription: string | null
internalNotes: string | null internalNotes: string | null
category: string | null
categoryCwId: number | null
subcategory: string | null
subcategoryCwId: number | null
manufacturer: string | null manufacturer: string | null
manufactureCwId: number | null manufactureCwId: number | null
partNumber: string | null partNumber: string | null
@@ -1729,10 +1931,15 @@ export interface Prisma__CatalogItemClient<T, Null = never, ExtArgs extends runt
export interface CatalogItemFieldRefs { export interface CatalogItemFieldRefs {
readonly id: Prisma.FieldRef<"CatalogItem", 'String'> readonly id: Prisma.FieldRef<"CatalogItem", 'String'>
readonly cwCatalogId: Prisma.FieldRef<"CatalogItem", 'Int'> readonly cwCatalogId: Prisma.FieldRef<"CatalogItem", 'Int'>
readonly identifier: Prisma.FieldRef<"CatalogItem", 'String'>
readonly name: Prisma.FieldRef<"CatalogItem", 'String'> readonly name: Prisma.FieldRef<"CatalogItem", 'String'>
readonly description: Prisma.FieldRef<"CatalogItem", 'String'> readonly description: Prisma.FieldRef<"CatalogItem", 'String'>
readonly customerDescription: Prisma.FieldRef<"CatalogItem", 'String'> readonly customerDescription: Prisma.FieldRef<"CatalogItem", 'String'>
readonly internalNotes: Prisma.FieldRef<"CatalogItem", 'String'> readonly internalNotes: Prisma.FieldRef<"CatalogItem", 'String'>
readonly category: Prisma.FieldRef<"CatalogItem", 'String'>
readonly categoryCwId: Prisma.FieldRef<"CatalogItem", 'Int'>
readonly subcategory: Prisma.FieldRef<"CatalogItem", 'String'>
readonly subcategoryCwId: Prisma.FieldRef<"CatalogItem", 'Int'>
readonly manufacturer: Prisma.FieldRef<"CatalogItem", 'String'> readonly manufacturer: Prisma.FieldRef<"CatalogItem", 'String'>
readonly manufactureCwId: Prisma.FieldRef<"CatalogItem", 'Int'> readonly manufactureCwId: Prisma.FieldRef<"CatalogItem", 'Int'>
readonly partNumber: Prisma.FieldRef<"CatalogItem", 'String'> readonly partNumber: Prisma.FieldRef<"CatalogItem", 'String'>
+128
View File
@@ -226,6 +226,7 @@ export type CompanyWhereInput = {
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
credentials?: Prisma.CredentialListRelationFilter credentials?: Prisma.CredentialListRelationFilter
unifiSites?: Prisma.UnifiSiteListRelationFilter unifiSites?: Prisma.UnifiSiteListRelationFilter
opportunities?: Prisma.OpportunityListRelationFilter
} }
export type CompanyOrderByWithRelationInput = { export type CompanyOrderByWithRelationInput = {
@@ -237,6 +238,7 @@ export type CompanyOrderByWithRelationInput = {
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
credentials?: Prisma.CredentialOrderByRelationAggregateInput credentials?: Prisma.CredentialOrderByRelationAggregateInput
unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput
opportunities?: Prisma.OpportunityOrderByRelationAggregateInput
} }
export type CompanyWhereUniqueInput = Prisma.AtLeast<{ export type CompanyWhereUniqueInput = Prisma.AtLeast<{
@@ -251,6 +253,7 @@ export type CompanyWhereUniqueInput = Prisma.AtLeast<{
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
credentials?: Prisma.CredentialListRelationFilter credentials?: Prisma.CredentialListRelationFilter
unifiSites?: Prisma.UnifiSiteListRelationFilter unifiSites?: Prisma.UnifiSiteListRelationFilter
opportunities?: Prisma.OpportunityListRelationFilter
}, "id" | "cw_CompanyId" | "cw_Identifier"> }, "id" | "cw_CompanyId" | "cw_Identifier">
export type CompanyOrderByWithAggregationInput = { export type CompanyOrderByWithAggregationInput = {
@@ -288,6 +291,7 @@ export type CompanyCreateInput = {
updatedAt?: Date | string updatedAt?: Date | string
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
} }
export type CompanyUncheckedCreateInput = { export type CompanyUncheckedCreateInput = {
@@ -299,6 +303,7 @@ export type CompanyUncheckedCreateInput = {
updatedAt?: Date | string updatedAt?: Date | string
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
} }
export type CompanyUpdateInput = { export type CompanyUpdateInput = {
@@ -310,6 +315,7 @@ export type CompanyUpdateInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
} }
export type CompanyUncheckedUpdateInput = { export type CompanyUncheckedUpdateInput = {
@@ -321,6 +327,7 @@ export type CompanyUncheckedUpdateInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
} }
export type CompanyCreateManyInput = { export type CompanyCreateManyInput = {
@@ -419,6 +426,22 @@ export type IntFieldUpdateOperationsInput = {
divide?: number divide?: number
} }
export type CompanyCreateNestedOneWithoutOpportunitiesInput = {
create?: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutOpportunitiesInput
connect?: Prisma.CompanyWhereUniqueInput
}
export type CompanyUpdateOneWithoutOpportunitiesNestedInput = {
create?: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutOpportunitiesInput
upsert?: Prisma.CompanyUpsertWithoutOpportunitiesInput
disconnect?: Prisma.CompanyWhereInput | boolean
delete?: Prisma.CompanyWhereInput | boolean
connect?: Prisma.CompanyWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.CompanyUpdateToOneWithWhereWithoutOpportunitiesInput, Prisma.CompanyUpdateWithoutOpportunitiesInput>, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
}
export type CompanyCreateNestedOneWithoutCredentialsInput = { export type CompanyCreateNestedOneWithoutCredentialsInput = {
create?: Prisma.XOR<Prisma.CompanyCreateWithoutCredentialsInput, Prisma.CompanyUncheckedCreateWithoutCredentialsInput> create?: Prisma.XOR<Prisma.CompanyCreateWithoutCredentialsInput, Prisma.CompanyUncheckedCreateWithoutCredentialsInput>
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutCredentialsInput connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutCredentialsInput
@@ -441,6 +464,7 @@ export type CompanyCreateWithoutUnifiSitesInput = {
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
} }
export type CompanyUncheckedCreateWithoutUnifiSitesInput = { export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
@@ -451,6 +475,7 @@ export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
} }
export type CompanyCreateOrConnectWithoutUnifiSitesInput = { export type CompanyCreateOrConnectWithoutUnifiSitesInput = {
@@ -477,6 +502,7 @@ export type CompanyUpdateWithoutUnifiSitesInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
} }
export type CompanyUncheckedUpdateWithoutUnifiSitesInput = { export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
@@ -487,6 +513,67 @@ export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
}
export type CompanyCreateWithoutOpportunitiesInput = {
id?: string
name: string
cw_CompanyId: number
cw_Identifier: string
createdAt?: Date | string
updatedAt?: Date | string
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
}
export type CompanyUncheckedCreateWithoutOpportunitiesInput = {
id?: string
name: string
cw_CompanyId: number
cw_Identifier: string
createdAt?: Date | string
updatedAt?: Date | string
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
}
export type CompanyCreateOrConnectWithoutOpportunitiesInput = {
where: Prisma.CompanyWhereUniqueInput
create: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
}
export type CompanyUpsertWithoutOpportunitiesInput = {
update: Prisma.XOR<Prisma.CompanyUpdateWithoutOpportunitiesInput, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
create: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
where?: Prisma.CompanyWhereInput
}
export type CompanyUpdateToOneWithWhereWithoutOpportunitiesInput = {
where?: Prisma.CompanyWhereInput
data: Prisma.XOR<Prisma.CompanyUpdateWithoutOpportunitiesInput, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
}
export type CompanyUpdateWithoutOpportunitiesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
cw_CompanyId?: Prisma.IntFieldUpdateOperationsInput | number
cw_Identifier?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
}
export type CompanyUncheckedUpdateWithoutOpportunitiesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
cw_CompanyId?: Prisma.IntFieldUpdateOperationsInput | number
cw_Identifier?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
} }
export type CompanyCreateWithoutCredentialsInput = { export type CompanyCreateWithoutCredentialsInput = {
@@ -497,6 +584,7 @@ export type CompanyCreateWithoutCredentialsInput = {
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
} }
export type CompanyUncheckedCreateWithoutCredentialsInput = { export type CompanyUncheckedCreateWithoutCredentialsInput = {
@@ -507,6 +595,7 @@ export type CompanyUncheckedCreateWithoutCredentialsInput = {
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
} }
export type CompanyCreateOrConnectWithoutCredentialsInput = { export type CompanyCreateOrConnectWithoutCredentialsInput = {
@@ -533,6 +622,7 @@ export type CompanyUpdateWithoutCredentialsInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
} }
export type CompanyUncheckedUpdateWithoutCredentialsInput = { export type CompanyUncheckedUpdateWithoutCredentialsInput = {
@@ -543,6 +633,7 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
} }
@@ -553,11 +644,13 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = {
export type CompanyCountOutputType = { export type CompanyCountOutputType = {
credentials: number credentials: number
unifiSites: number unifiSites: number
opportunities: number
} }
export type CompanyCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type CompanyCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs
unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs
opportunities?: boolean | CompanyCountOutputTypeCountOpportunitiesArgs
} }
/** /**
@@ -584,6 +677,13 @@ export type CompanyCountOutputTypeCountUnifiSitesArgs<ExtArgs extends runtime.Ty
where?: Prisma.UnifiSiteWhereInput where?: Prisma.UnifiSiteWhereInput
} }
/**
* CompanyCountOutputType without action
*/
export type CompanyCountOutputTypeCountOpportunitiesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.OpportunityWhereInput
}
export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
@@ -594,6 +694,7 @@ export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
updatedAt?: boolean updatedAt?: boolean
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs> credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs> unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["company"]> }, ExtArgs["result"]["company"]>
@@ -628,6 +729,7 @@ export type CompanyOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs =
export type CompanyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type CompanyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs> credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs> unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
} }
export type CompanyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {} export type CompanyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
@@ -638,6 +740,7 @@ export type $CompanyPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
objects: { objects: {
credentials: Prisma.$CredentialPayload<ExtArgs>[] credentials: Prisma.$CredentialPayload<ExtArgs>[]
unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[] unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[]
opportunities: Prisma.$OpportunityPayload<ExtArgs>[]
} }
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string id: string
@@ -1042,6 +1145,7 @@ export interface Prisma__CompanyClient<T, Null = never, ExtArgs extends runtime.
readonly [Symbol.toStringTag]: "PrismaPromise" readonly [Symbol.toStringTag]: "PrismaPromise"
credentials<T extends Prisma.Company$credentialsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$credentialsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CredentialPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null> credentials<T extends Prisma.Company$credentialsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$credentialsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CredentialPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
unifiSites<T extends Prisma.Company$unifiSitesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$unifiSitesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UnifiSitePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null> unifiSites<T extends Prisma.Company$unifiSitesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$unifiSitesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UnifiSitePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
opportunities<T extends Prisma.Company$opportunitiesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$opportunitiesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$OpportunityPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/** /**
* Attaches callbacks for the resolution and/or rejection of the Promise. * Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved. * @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1512,6 +1616,30 @@ export type Company$unifiSitesArgs<ExtArgs extends runtime.Types.Extensions.Inte
distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[] distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[]
} }
/**
* Company.opportunities
*/
export type Company$opportunitiesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Opportunity
*/
select?: Prisma.OpportunitySelect<ExtArgs> | null
/**
* Omit specific fields from the Opportunity
*/
omit?: Prisma.OpportunityOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.OpportunityInclude<ExtArgs> | null
where?: Prisma.OpportunityWhereInput
orderBy?: Prisma.OpportunityOrderByWithRelationInput | Prisma.OpportunityOrderByWithRelationInput[]
cursor?: Prisma.OpportunityWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.OpportunityScalarFieldEnum | Prisma.OpportunityScalarFieldEnum[]
}
/** /**
* Company without action * Company without action
*/ */
File diff suppressed because it is too large Load Diff
+39 -1
View File
@@ -32,6 +32,7 @@ export type UserMinAggregateOutputType = {
email: string | null email: string | null
emailVerified: Date | null emailVerified: Date | null
image: string | null image: string | null
cwIdentifier: string | null
userId: string | null userId: string | null
token: string | null token: string | null
createdAt: Date | null createdAt: Date | null
@@ -46,6 +47,7 @@ export type UserMaxAggregateOutputType = {
email: string | null email: string | null
emailVerified: Date | null emailVerified: Date | null
image: string | null image: string | null
cwIdentifier: string | null
userId: string | null userId: string | null
token: string | null token: string | null
createdAt: Date | null createdAt: Date | null
@@ -60,6 +62,7 @@ export type UserCountAggregateOutputType = {
email: number email: number
emailVerified: number emailVerified: number
image: number image: number
cwIdentifier: number
userId: number userId: number
token: number token: number
createdAt: number createdAt: number
@@ -76,6 +79,7 @@ export type UserMinAggregateInputType = {
email?: true email?: true
emailVerified?: true emailVerified?: true
image?: true image?: true
cwIdentifier?: true
userId?: true userId?: true
token?: true token?: true
createdAt?: true createdAt?: true
@@ -90,6 +94,7 @@ export type UserMaxAggregateInputType = {
email?: true email?: true
emailVerified?: true emailVerified?: true
image?: true image?: true
cwIdentifier?: true
userId?: true userId?: true
token?: true token?: true
createdAt?: true createdAt?: true
@@ -104,6 +109,7 @@ export type UserCountAggregateInputType = {
email?: true email?: true
emailVerified?: true emailVerified?: true
image?: true image?: true
cwIdentifier?: true
userId?: true userId?: true
token?: true token?: true
createdAt?: true createdAt?: true
@@ -191,6 +197,7 @@ export type UserGroupByOutputType = {
email: string email: string
emailVerified: Date | null emailVerified: Date | null
image: string | null image: string | null
cwIdentifier: string | null
userId: string userId: string
token: string | null token: string | null
createdAt: Date createdAt: Date
@@ -226,6 +233,7 @@ export type UserWhereInput = {
email?: Prisma.StringFilter<"User"> | string email?: Prisma.StringFilter<"User"> | string
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
image?: Prisma.StringNullableFilter<"User"> | string | null image?: Prisma.StringNullableFilter<"User"> | string | null
cwIdentifier?: Prisma.StringNullableFilter<"User"> | string | null
userId?: Prisma.StringFilter<"User"> | string userId?: Prisma.StringFilter<"User"> | string
token?: Prisma.StringNullableFilter<"User"> | string | null token?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
@@ -242,6 +250,7 @@ export type UserOrderByWithRelationInput = {
email?: Prisma.SortOrder email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrderInput | Prisma.SortOrder emailVerified?: Prisma.SortOrderInput | Prisma.SortOrder
image?: Prisma.SortOrderInput | Prisma.SortOrder image?: Prisma.SortOrderInput | Prisma.SortOrder
cwIdentifier?: Prisma.SortOrderInput | Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
token?: Prisma.SortOrderInput | Prisma.SortOrder token?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
@@ -262,6 +271,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
name?: Prisma.StringNullableFilter<"User"> | string | null name?: Prisma.StringNullableFilter<"User"> | string | null
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
image?: Prisma.StringNullableFilter<"User"> | string | null image?: Prisma.StringNullableFilter<"User"> | string | null
cwIdentifier?: Prisma.StringNullableFilter<"User"> | string | null
token?: Prisma.StringNullableFilter<"User"> | string | null token?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
@@ -277,6 +287,7 @@ export type UserOrderByWithAggregationInput = {
email?: Prisma.SortOrder email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrderInput | Prisma.SortOrder emailVerified?: Prisma.SortOrderInput | Prisma.SortOrder
image?: Prisma.SortOrderInput | Prisma.SortOrder image?: Prisma.SortOrderInput | Prisma.SortOrder
cwIdentifier?: Prisma.SortOrderInput | Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
token?: Prisma.SortOrderInput | Prisma.SortOrder token?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
@@ -297,6 +308,7 @@ export type UserScalarWhereWithAggregatesInput = {
email?: Prisma.StringWithAggregatesFilter<"User"> | string email?: Prisma.StringWithAggregatesFilter<"User"> | string
emailVerified?: Prisma.DateTimeNullableWithAggregatesFilter<"User"> | Date | string | null emailVerified?: Prisma.DateTimeNullableWithAggregatesFilter<"User"> | Date | string | null
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
cwIdentifier?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
userId?: Prisma.StringWithAggregatesFilter<"User"> | string userId?: Prisma.StringWithAggregatesFilter<"User"> | string
token?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null token?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
@@ -311,6 +323,7 @@ export type UserCreateInput = {
email: string email: string
emailVerified?: Date | string | null emailVerified?: Date | string | null
image?: string | null image?: string | null
cwIdentifier?: string | null
userId: string userId: string
token?: string | null token?: string | null
createdAt?: Date | string createdAt?: Date | string
@@ -327,6 +340,7 @@ export type UserUncheckedCreateInput = {
email: string email: string
emailVerified?: Date | string | null emailVerified?: Date | string | null
image?: string | null image?: string | null
cwIdentifier?: string | null
userId: string userId: string
token?: string | null token?: string | null
createdAt?: Date | string createdAt?: Date | string
@@ -343,6 +357,7 @@ export type UserUpdateInput = {
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -359,6 +374,7 @@ export type UserUncheckedUpdateInput = {
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -375,6 +391,7 @@ export type UserCreateManyInput = {
email: string email: string
emailVerified?: Date | string | null emailVerified?: Date | string | null
image?: string | null image?: string | null
cwIdentifier?: string | null
userId: string userId: string
token?: string | null token?: string | null
createdAt?: Date | string createdAt?: Date | string
@@ -389,6 +406,7 @@ export type UserUpdateManyMutationInput = {
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -403,6 +421,7 @@ export type UserUncheckedUpdateManyInput = {
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -422,6 +441,7 @@ export type UserCountOrderByAggregateInput = {
email?: Prisma.SortOrder email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder image?: Prisma.SortOrder
cwIdentifier?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
token?: Prisma.SortOrder token?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
@@ -436,6 +456,7 @@ export type UserMaxOrderByAggregateInput = {
email?: Prisma.SortOrder email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder image?: Prisma.SortOrder
cwIdentifier?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
token?: Prisma.SortOrder token?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
@@ -450,6 +471,7 @@ export type UserMinOrderByAggregateInput = {
email?: Prisma.SortOrder email?: Prisma.SortOrder
emailVerified?: Prisma.SortOrder emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder image?: Prisma.SortOrder
cwIdentifier?: Prisma.SortOrder
userId?: Prisma.SortOrder userId?: Prisma.SortOrder
token?: Prisma.SortOrder token?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
@@ -530,6 +552,7 @@ export type UserCreateWithoutSessionsInput = {
email: string email: string
emailVerified?: Date | string | null emailVerified?: Date | string | null
image?: string | null image?: string | null
cwIdentifier?: string | null
userId: string userId: string
token?: string | null token?: string | null
createdAt?: Date | string createdAt?: Date | string
@@ -545,6 +568,7 @@ export type UserUncheckedCreateWithoutSessionsInput = {
email: string email: string
emailVerified?: Date | string | null emailVerified?: Date | string | null
image?: string | null image?: string | null
cwIdentifier?: string | null
userId: string userId: string
token?: string | null token?: string | null
createdAt?: Date | string createdAt?: Date | string
@@ -576,6 +600,7 @@ export type UserUpdateWithoutSessionsInput = {
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -591,6 +616,7 @@ export type UserUncheckedUpdateWithoutSessionsInput = {
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -606,6 +632,7 @@ export type UserCreateWithoutRolesInput = {
email: string email: string
emailVerified?: Date | string | null emailVerified?: Date | string | null
image?: string | null image?: string | null
cwIdentifier?: string | null
userId: string userId: string
token?: string | null token?: string | null
createdAt?: Date | string createdAt?: Date | string
@@ -621,6 +648,7 @@ export type UserUncheckedCreateWithoutRolesInput = {
email: string email: string
emailVerified?: Date | string | null emailVerified?: Date | string | null
image?: string | null image?: string | null
cwIdentifier?: string | null
userId: string userId: string
token?: string | null token?: string | null
createdAt?: Date | string createdAt?: Date | string
@@ -660,6 +688,7 @@ export type UserScalarWhereInput = {
email?: Prisma.StringFilter<"User"> | string email?: Prisma.StringFilter<"User"> | string
emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null emailVerified?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
image?: Prisma.StringNullableFilter<"User"> | string | null image?: Prisma.StringNullableFilter<"User"> | string | null
cwIdentifier?: Prisma.StringNullableFilter<"User"> | string | null
userId?: Prisma.StringFilter<"User"> | string userId?: Prisma.StringFilter<"User"> | string
token?: Prisma.StringNullableFilter<"User"> | string | null token?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
@@ -674,6 +703,7 @@ export type UserUpdateWithoutRolesInput = {
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -689,6 +719,7 @@ export type UserUncheckedUpdateWithoutRolesInput = {
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -704,6 +735,7 @@ export type UserUncheckedUpdateManyWithoutRolesInput = {
email?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
cwIdentifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
userId?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string
token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null token?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -758,6 +790,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
email?: boolean email?: boolean
emailVerified?: boolean emailVerified?: boolean
image?: boolean image?: boolean
cwIdentifier?: boolean
userId?: boolean userId?: boolean
token?: boolean token?: boolean
createdAt?: boolean createdAt?: boolean
@@ -775,6 +808,7 @@ export type UserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensio
email?: boolean email?: boolean
emailVerified?: boolean emailVerified?: boolean
image?: boolean image?: boolean
cwIdentifier?: boolean
userId?: boolean userId?: boolean
token?: boolean token?: boolean
createdAt?: boolean createdAt?: boolean
@@ -789,6 +823,7 @@ export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensio
email?: boolean email?: boolean
emailVerified?: boolean emailVerified?: boolean
image?: boolean image?: boolean
cwIdentifier?: boolean
userId?: boolean userId?: boolean
token?: boolean token?: boolean
createdAt?: boolean createdAt?: boolean
@@ -803,13 +838,14 @@ export type UserSelectScalar = {
email?: boolean email?: boolean
emailVerified?: boolean emailVerified?: boolean
image?: boolean image?: boolean
cwIdentifier?: boolean
userId?: boolean userId?: boolean
token?: boolean token?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
} }
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "permissions" | "login" | "name" | "email" | "emailVerified" | "image" | "userId" | "token" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]> export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "permissions" | "login" | "name" | "email" | "emailVerified" | "image" | "cwIdentifier" | "userId" | "token" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]>
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
roles?: boolean | Prisma.User$rolesArgs<ExtArgs> roles?: boolean | Prisma.User$rolesArgs<ExtArgs>
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs> sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
@@ -832,6 +868,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
email: string email: string
emailVerified: Date | null emailVerified: Date | null
image: string | null image: string | null
cwIdentifier: string | null
userId: string userId: string
token: string | null token: string | null
createdAt: Date createdAt: Date
@@ -1268,6 +1305,7 @@ export interface UserFieldRefs {
readonly email: Prisma.FieldRef<"User", 'String'> readonly email: Prisma.FieldRef<"User", 'String'>
readonly emailVerified: Prisma.FieldRef<"User", 'DateTime'> readonly emailVerified: Prisma.FieldRef<"User", 'DateTime'>
readonly image: Prisma.FieldRef<"User", 'String'> readonly image: Prisma.FieldRef<"User", 'String'>
readonly cwIdentifier: Prisma.FieldRef<"User", 'String'>
readonly userId: Prisma.FieldRef<"User", 'String'> readonly userId: Prisma.FieldRef<"User", 'String'>
readonly token: Prisma.FieldRef<"User", 'String'> readonly token: Prisma.FieldRef<"User", 'String'>
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'> readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
+4 -1
View File
@@ -19,13 +19,15 @@
}, },
"scripts": { "scripts": {
"dev": "NODE_ENV=development bun --watch src/index.ts", "dev": "NODE_ENV=development bun --watch src/index.ts",
"test": "bun test --preload ./tests/setup.ts",
"db:gen": "prisma generate", "db:gen": "prisma generate",
"db:push": "prisma migrate dev --skip-generate", "db:push": "prisma migrate dev --skip-generate",
"db:deploy": "prisma migrate deploy", "db:deploy": "prisma migrate deploy",
"utils:dev": "docker compose -f .docker/docker-compose.yml up --build", "utils:dev": "docker compose -f .docker/docker-compose.yml up --build",
"utils:gen_private_keys": "bun ./utils/genPrivateKeys", "utils:gen_private_keys": "bun ./utils/genPrivateKeys",
"utils:create_admin_role": "bun ./utils/createAdminRole", "utils:create_admin_role": "bun ./utils/createAdminRole",
"utils:assign_user_role": "bun ./utils/assignUserRole" "utils:assign_user_role": "bun ./utils/assignUserRole",
"db:check": "bunx prisma migrate diff --from-migrations prisma/migrations --to-schema prisma/schema.prisma --shadow-database-url $DATABASE_URL --exit-code"
}, },
"dependencies": { "dependencies": {
"@azure/msal-node": "^5.0.2", "@azure/msal-node": "^5.0.2",
@@ -39,6 +41,7 @@
"cors": "^2.8.6", "cors": "^2.8.6",
"cuid": "^3.0.0", "cuid": "^3.0.0",
"hono": "^4.11.5", "hono": "^4.11.5",
"ioredis": "^5.10.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"keypair": "^1.0.4", "keypair": "^1.0.4",
"prisma": "^7.3.0", "prisma": "^7.3.0",
+23
View File
@@ -0,0 +1,23 @@
#!/bin/sh
set -e
# ---------------------------------------------------------------------------
# 1. Resolve any previously failed migrations so deploy can proceed.
# Only migrations explicitly marked as "Failed" in the status output are
# resolved. We grep for lines containing "Failed" and extract the name.
# ---------------------------------------------------------------------------
echo "[migrate] Checking for failed migrations..."
STATUS_OUTPUT=$(bunx prisma migrate status 2>&1 || true)
echo "$STATUS_OUTPUT"
# Only resolve migrations whose status line explicitly says "Failed"
echo "$STATUS_OUTPUT" | grep -i "failed" | grep -oE '[0-9]{14}_[a-zA-Z_]+' | while read -r MIGRATION; do
echo "[migrate] Resolving failed migration: $MIGRATION"
bunx prisma migrate resolve --rolled-back "$MIGRATION" || true
done
# ---------------------------------------------------------------------------
# 2. Deploy all pending migrations from the migrations directory.
# ---------------------------------------------------------------------------
echo "[migrate] Running prisma migrate deploy..."
bunx prisma migrate deploy
@@ -0,0 +1,63 @@
-- CreateTable
CREATE TABLE "UnifiSite" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"siteId" TEXT NOT NULL,
"companyId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UnifiSite_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CatalogItem" (
"id" TEXT NOT NULL,
"cwCatalogId" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"customerDescription" TEXT,
"internalNotes" TEXT,
"manufacturer" TEXT,
"manufactureCwId" INTEGER,
"partNumber" TEXT,
"vendorName" TEXT,
"vendorSku" TEXT,
"vendorCwId" INTEGER,
"price" DOUBLE PRECISION NOT NULL,
"cost" DOUBLE PRECISION NOT NULL,
"inactive" BOOLEAN NOT NULL DEFAULT false,
"salesTaxable" BOOLEAN NOT NULL DEFAULT true,
"onHand" INTEGER NOT NULL DEFAULT 0,
"cwLastUpdated" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CatalogItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_LinkedItems" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_LinkedItems_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE UNIQUE INDEX "UnifiSite_siteId_key" ON "UnifiSite"("siteId");
-- CreateIndex
CREATE UNIQUE INDEX "CatalogItem_cwCatalogId_key" ON "CatalogItem"("cwCatalogId");
-- CreateIndex
CREATE INDEX "_LinkedItems_B_index" ON "_LinkedItems"("B");
-- AddForeignKey
ALTER TABLE "UnifiSite" ADD CONSTRAINT "UnifiSite_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_LinkedItems" ADD CONSTRAINT "_LinkedItems_A_fkey" FOREIGN KEY ("A") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_LinkedItems" ADD CONSTRAINT "_LinkedItems_B_fkey" FOREIGN KEY ("B") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,57 @@
-- CreateTable
CREATE TABLE "Opportunity" (
"id" TEXT NOT NULL,
"cwOpportunityId" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"notes" TEXT,
"typeName" TEXT,
"typeCwId" INTEGER,
"stageName" TEXT,
"stageCwId" INTEGER,
"statusName" TEXT,
"statusCwId" INTEGER,
"priorityName" TEXT,
"priorityCwId" INTEGER,
"ratingName" TEXT,
"ratingCwId" INTEGER,
"source" TEXT,
"campaignName" TEXT,
"campaignCwId" INTEGER,
"primarySalesRepName" TEXT,
"primarySalesRepIdentifier" TEXT,
"primarySalesRepCwId" INTEGER,
"secondarySalesRepName" TEXT,
"secondarySalesRepIdentifier" TEXT,
"secondarySalesRepCwId" INTEGER,
"companyCwId" INTEGER,
"companyName" TEXT,
"contactCwId" INTEGER,
"contactName" TEXT,
"siteCwId" INTEGER,
"siteName" TEXT,
"customerPO" TEXT,
"totalSalesTax" DOUBLE PRECISION NOT NULL DEFAULT 0,
"locationName" TEXT,
"locationCwId" INTEGER,
"departmentName" TEXT,
"departmentCwId" INTEGER,
"expectedCloseDate" TIMESTAMP(3),
"pipelineChangeDate" TIMESTAMP(3),
"dateBecameLead" TIMESTAMP(3),
"closedDate" TIMESTAMP(3),
"closedFlag" BOOLEAN NOT NULL DEFAULT false,
"closedByName" TEXT,
"closedByCwId" INTEGER,
"companyId" TEXT,
"cwLastUpdated" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Opportunity_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Opportunity_cwOpportunityId_key" ON "Opportunity"("cwOpportunityId");
-- AddForeignKey
ALTER TABLE "Opportunity" ADD CONSTRAINT "Opportunity_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "CatalogItem" ADD COLUMN "identifier" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "CatalogItem_identifier_key" ON "CatalogItem"("identifier");
+80
View File
@@ -34,6 +34,8 @@ model User {
emailVerified DateTime? emailVerified DateTime?
image String? image String?
cwIdentifier String?
userId String @unique userId String @unique
token String? token String?
@@ -77,6 +79,7 @@ model Company {
credentials Credential[] credentials Credential[]
unifiSites UnifiSite[] unifiSites UnifiSite[]
opportunities Opportunity[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -85,6 +88,7 @@ model Company {
model CatalogItem { model CatalogItem {
id String @id @default(cuid()) id String @id @default(cuid())
cwCatalogId Int @unique cwCatalogId Int @unique
identifier String? @unique
name String name String
description String? description String?
customerDescription String? customerDescription String?
@@ -93,6 +97,11 @@ model CatalogItem {
linkedItems CatalogItem[] @relation("LinkedItems") linkedItems CatalogItem[] @relation("LinkedItems")
linkedTo CatalogItem[] @relation("LinkedItems") linkedTo CatalogItem[] @relation("LinkedItems")
category String?
categoryCwId Int?
subcategory String?
subcategoryCwId Int?
manufacturer String? manufacturer String?
manufactureCwId Int? manufactureCwId Int?
@@ -115,6 +124,77 @@ model CatalogItem {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Opportunity {
id String @id @default(cuid())
cwOpportunityId Int @unique
name String
notes String?
// Stage / status / priority / type / rating stored as JSON references
// so we don't need separate lookup tables for CW enums
typeName String?
typeCwId Int?
stageName String?
stageCwId Int?
statusName String?
statusCwId Int?
priorityName String?
priorityCwId Int?
ratingName String?
ratingCwId Int?
source String?
campaignName String?
campaignCwId Int?
// Sales rep references
primarySalesRepName String?
primarySalesRepIdentifier String?
primarySalesRepCwId Int?
secondarySalesRepName String?
secondarySalesRepIdentifier String?
secondarySalesRepCwId Int?
// Company / contact / site
companyCwId Int?
companyName String?
contactCwId Int?
contactName String?
siteCwId Int?
siteName String?
customerPO String?
// Financials
totalSalesTax Float @default(0)
// Location / department
locationName String?
locationCwId Int?
departmentName String?
departmentCwId Int?
// Dates
expectedCloseDate DateTime?
pipelineChangeDate DateTime?
dateBecameLead DateTime?
closedDate DateTime?
closedFlag Boolean @default(false)
closedByName String?
closedByCwId Int?
// Internal relation to Company (optional, linked by cwCompanyId)
companyId String?
company Company? @relation(fields: [companyId], references: [id])
// Local product sequence — array of CW forecast item IDs in display order.
// When present, fetchProducts() uses this order instead of CW sequenceNumber.
productSequence Int[] @default([])
cwLastUpdated DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CredentialType { model CredentialType {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String @unique
+2 -2
View File
@@ -1,7 +1,7 @@
import { Hono } from "hono/tiny"; import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute"; import { createRoute } from "../../modules/api-utils/createRoute";
import * as msal from "@azure/msal-node"; import * as msal from "@azure/msal-node";
import { io, msalClient } from "../../constants"; import { API_BASE_URL, io, msalClient } from "../../constants";
import { users } from "../../managers/users"; import { users } from "../../managers/users";
/* /v1/auth/redirect */ /* /v1/auth/redirect */
@@ -11,7 +11,7 @@ export default createRoute("get", ["/redirect"], async (c) => {
const tokenRequest: msal.AuthorizationCodeRequest = { const tokenRequest: msal.AuthorizationCodeRequest = {
code: c.req.query().code as string, code: c.req.query().code as string,
scopes: ["user.read"], scopes: ["user.read"],
redirectUri: "http://localhost:3000/v1/auth/redirect", redirectUri: `${API_BASE_URL}/v1/auth/redirect`,
}; };
const authResult = await msalClient.acquireTokenByCode(tokenRequest); const authResult = await msalClient.acquireTokenByCode(tokenRequest);
+3 -1
View File
@@ -1,5 +1,6 @@
import { Hono } from "hono/tiny"; import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute"; import { createRoute } from "../../modules/api-utils/createRoute";
import { API_BASE_URL } from "../../constants";
import cuid from "cuid"; import cuid from "cuid";
/* /v1/auth/uri */ /* /v1/auth/uri */
@@ -7,7 +8,8 @@ export default createRoute("get", ["/uri"], (c) => {
c.status(200); c.status(200);
const callbackKey = cuid(); const callbackKey = cuid();
const msUri = `https://login.microsoftonline.com/${process.env.MICROSOFT_TENANT_ID}/oauth2/v2.0/authorize?client_id=${process.env.MICROSOFT_CLIENT_ID}&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fv1%2Fauth%2Fredirect&scope=openid+User.Read&state=${callbackKey}&prompt=login`; const redirectUri = encodeURIComponent(`${API_BASE_URL}/v1/auth/redirect`);
const msUri = `https://login.microsoftonline.com/${process.env.MICROSOFT_TENANT_ID}/oauth2/v2.0/authorize?client_id=${process.env.MICROSOFT_CLIENT_ID}&response_type=code&redirect_uri=${redirectUri}&scope=openid+User.Read&state=${callbackKey}&prompt=login`;
return c.json({ return c.json({
status: 200, status: 200,
+12 -4
View File
@@ -5,6 +5,7 @@ import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization"; import { authMiddleware } from "../../middleware/authorization";
import GenericError from "../../../Errors/GenericError"; import GenericError from "../../../Errors/GenericError";
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
/* /v1/company/companies/[id] */ /* /v1/company/companies/[id] */
export default createRoute( export default createRoute(
@@ -42,13 +43,20 @@ export default createRoute(
} }
} }
const response = apiResponse.successful( const companyData = company.toJson({
"Company Fetched Successfully!",
company.toJson({
includeAddress, includeAddress,
includePrimaryContact, includePrimaryContact,
includeAllContacts, includeAllContacts,
}), });
const gatedData = await processObjectValuePerms(
companyData,
"obj.company",
c.get("user"),
);
const response = apiResponse.successful(
"Company Fetched Successfully!",
gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+9 -1
View File
@@ -4,6 +4,7 @@ import { companies } from "../../../managers/companies";
import { apiResponse } from "../../../modules/api-utils/apiResponse"; import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization"; import { authMiddleware } from "../../middleware/authorization";
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
/* GET /v1/company/companies/:identifier/unifi/sites */ /* GET /v1/company/companies/:identifier/unifi/sites */
export default createRoute( export default createRoute(
@@ -12,9 +13,16 @@ export default createRoute(
async (c) => { async (c) => {
const company = await companies.fetch(c.req.param("identifier")); const company = await companies.fetch(c.req.param("identifier"));
const sites = await unifiSites.fetchByCompany(company.id); const sites = await unifiSites.fetchByCompany(company.id);
const gatedData = await Promise.all(
sites.map((site) =>
processObjectValuePerms(site, "obj.unifiSite", c.get("user")),
),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"Company UniFi Sites Fetched Successfully!", "Company UniFi Sites Fetched Successfully!",
sites, gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+8 -1
View File
@@ -4,6 +4,7 @@ import { companies } from "../../managers/companies";
import { apiResponse } from "../../modules/api-utils/apiResponse"; import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* /v1/company/companies */ /* /v1/company/companies */
export default createRoute( export default createRoute(
@@ -22,9 +23,15 @@ export default createRoute(
? (await companies.search(search, 1, 999999)).length ? (await companies.search(search, 1, 999999)).length
: await companies.count(); : await companies.count();
const gatedData = await Promise.all(
data.map((item) =>
processObjectValuePerms(item, "obj.company", c.get("user")),
),
);
let response = apiResponse.successful( let response = apiResponse.successful(
"Companies Fetched Successfully!", "Companies Fetched Successfully!",
data, gatedData,
{ {
pagination: { pagination: {
previousPage: page == 1 ? null : page - 1, // Previous Page previousPage: page == 1 ? null : page - 1, // Previous Page
+8 -1
View File
@@ -4,6 +4,7 @@ import { credentialTypes } from "../../managers/credentialTypes";
import { apiResponse } from "../../modules/api-utils/apiResponse"; import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* /v1/credential-type/:identifier */ /* /v1/credential-type/:identifier */
export default createRoute( export default createRoute(
@@ -15,9 +16,15 @@ export default createRoute(
c.req.param("identifier"), c.req.param("identifier"),
); );
const gatedData = await processObjectValuePerms(
credentialType.toJson({ includeCredentialCount: true }),
"obj.credentialType",
c.get("user"),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"Credential Type Fetched Successfully!", "Credential Type Fetched Successfully!",
credentialType.toJson({ includeCredentialCount: true }), gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+12 -3
View File
@@ -4,6 +4,7 @@ import { credentialTypes } from "../../managers/credentialTypes";
import { apiResponse } from "../../modules/api-utils/apiResponse"; import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* /v1/credential-type */ /* /v1/credential-type */
export default createRoute( export default createRoute(
@@ -13,11 +14,19 @@ export default createRoute(
async (c) => { async (c) => {
const allCredentialTypes = await credentialTypes.fetchAll(); const allCredentialTypes = await credentialTypes.fetchAll();
const gatedData = await Promise.all(
allCredentialTypes.map((ct) =>
processObjectValuePerms(
ct.toJson({ includeCredentialCount: true }),
"obj.credentialType",
c.get("user"),
),
),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"Credential Types Fetched Successfully!", "Credential Types Fetched Successfully!",
allCredentialTypes.map((ct) => gatedData,
ct.toJson({ includeCredentialCount: true }),
),
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+8 -1
View File
@@ -4,6 +4,7 @@ import { credentialTypes } from "../../managers/credentialTypes";
import { apiResponse } from "../../modules/api-utils/apiResponse"; import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* /v1/credential-type/:id/credentials */ /* /v1/credential-type/:id/credentials */
export default createRoute( export default createRoute(
@@ -14,9 +15,15 @@ export default createRoute(
const credentialType = await credentialTypes.fetch(c.req.param("id")); const credentialType = await credentialTypes.fetch(c.req.param("id"));
const credentials = await credentialType.fetchCredentials(); const credentials = await credentialType.fetchCredentials();
const gatedData = await Promise.all(
credentials.map((cred) =>
processObjectValuePerms(cred.toJson(), "obj.credential", c.get("user")),
),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"Credentials Fetched Successfully!", "Credentials Fetched Successfully!",
credentials.map((cred) => cred.toJson()), gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+7 -1
View File
@@ -4,6 +4,7 @@ import { credentials } from "../../managers/credentials";
import { apiResponse } from "../../modules/api-utils/apiResponse"; import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* /v1/credential/:id */ /* /v1/credential/:id */
export default createRoute( export default createRoute(
@@ -12,10 +13,15 @@ export default createRoute(
async (c) => { async (c) => {
const credential = await credentials.fetch(c.req.param("id")); const credential = await credentials.fetch(c.req.param("id"));
const gatedData = await processObjectValuePerms(
credential.toJson(),
"obj.credential",
c.get("user"),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"Credential Fetched Successfully!", "Credential Fetched Successfully!",
credential.toJson(), gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+8 -1
View File
@@ -4,6 +4,7 @@ import { credentials } from "../../managers/credentials";
import { apiResponse } from "../../modules/api-utils/apiResponse"; import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* /v1/credential/company/:companyId */ /* /v1/credential/company/:companyId */
export default createRoute( export default createRoute(
@@ -15,9 +16,15 @@ export default createRoute(
c.req.param("companyId"), c.req.param("companyId"),
); );
const gatedData = await Promise.all(
companyCredentials.map((cred) =>
processObjectValuePerms(cred.toJson(), "obj.credential", c.get("user")),
),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"Company Credentials Fetched Successfully!", "Company Credentials Fetched Successfully!",
companyCredentials.map((cred) => cred.toJson()), gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+8 -1
View File
@@ -3,6 +3,7 @@ import { credentials } from "../../managers/credentials";
import { apiResponse } from "../../modules/api-utils/apiResponse"; import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* GET /v1/credential/credentials/:id/sub-credentials */ /* GET /v1/credential/credentials/:id/sub-credentials */
export default createRoute( export default createRoute(
@@ -17,9 +18,15 @@ export default createRoute(
const subCredentials = await credentials.fetchSubCredentials(parentId); const subCredentials = await credentials.fetchSubCredentials(parentId);
const gatedData = await Promise.all(
subCredentials.map((sc) =>
processObjectValuePerms(sc.toJson(), "obj.credential", c.get("user")),
),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"Sub-Credentials Fetched Successfully!", "Sub-Credentials Fetched Successfully!",
subCredentials.map((sc) => sc.toJson()), gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+32
View File
@@ -0,0 +1,32 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { procurement } from "../../../managers/procurement";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
/* /v1/procurement/items/:identifier */
export default createRoute(
"get",
["/items/:identifier"],
async (c) => {
const identifier = c.req.param("identifier");
const includeLinkedItems = c.req.query("includeLinkedItems") === "true";
const item = await procurement.fetchItem(identifier);
const gatedData = await processObjectValuePerms(
item.toJson({ includeLinkedItems }),
"obj.catalogItem",
c.get("user"),
);
const response = apiResponse.successful(
"Catalog item fetched successfully!",
gatedData,
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["procurement.catalog.fetch"] }),
);
+32
View File
@@ -0,0 +1,32 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { procurement } from "../../../managers/procurement";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
/* GET /v1/procurement/items/:identifier/linked */
export default createRoute(
"get",
["/items/:identifier/linked"],
async (c) => {
const identifier = c.req.param("identifier");
const item = await procurement.fetchItem(identifier);
const linkedItems = item.getLinkedItems().map((linked) => linked.toJson());
const gatedData = await Promise.all(
linkedItems.map((linked) =>
processObjectValuePerms(linked, "obj.catalogItem", c.get("user")),
),
);
const response = apiResponse.successful(
"Linked catalog items fetched successfully!",
gatedData,
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["procurement.catalog.fetch"] }),
);
+28
View File
@@ -0,0 +1,28 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { procurement } from "../../../managers/procurement";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import { z } from "zod";
/* POST /v1/procurement/items/:identifier/link */
export default createRoute(
"post",
["/items/:identifier/link"],
async (c) => {
const identifier = c.req.param("identifier");
const body = await c.req.json();
const schema = z.object({ targetId: z.string() }).strict();
const { targetId } = schema.parse(body);
const item = await procurement.linkItems(identifier, targetId);
const response = apiResponse.successful(
"Catalog item linked successfully!",
item.toJson({ includeLinkedItems: true }),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["procurement.catalog.link"] }),
);
@@ -0,0 +1,25 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { procurement } from "../../../managers/procurement";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
/* /v1/procurement/items/:identifier/refresh-inventory */
export default createRoute(
"post",
["/items/:identifier/refresh-inventory"],
async (c) => {
const identifier = c.req.param("identifier");
const item = await procurement.fetchItem(identifier);
await item.refreshInventory();
const response = apiResponse.successful(
"Inventory refreshed successfully!",
item.toJson(),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["procurement.catalog.inventory.refresh"] }),
);
+28
View File
@@ -0,0 +1,28 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { procurement } from "../../../managers/procurement";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import { z } from "zod";
/* POST /v1/procurement/items/:identifier/unlink */
export default createRoute(
"post",
["/items/:identifier/unlink"],
async (c) => {
const identifier = c.req.param("identifier");
const body = await c.req.json();
const schema = z.object({ targetId: z.string() }).strict();
const { targetId } = schema.parse(body);
const item = await procurement.unlinkItems(identifier, targetId);
const response = apiResponse.successful(
"Catalog item unlinked successfully!",
item.toJson({ includeLinkedItems: true }),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["procurement.catalog.link"] }),
);
+26
View File
@@ -0,0 +1,26 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
import {
serializeCategoryTree,
serializeEcosystemTree,
} from "../../modules/catalog-categories/catalogCategories";
/* /v1/procurement/categories */
export default createRoute(
"get",
["/categories"],
async (c) => {
const categories = serializeCategoryTree();
const ecosystems = serializeEcosystemTree();
const response = apiResponse.successful(
"Category and ecosystem data fetched successfully!",
{ categories, ecosystems },
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
);
+24
View File
@@ -0,0 +1,24 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { procurement } from "../../managers/procurement";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
/* /v1/procurement/count */
export default createRoute(
"get",
["/count"],
async (c) => {
const activeOnly = c.req.query("activeOnly") === "true";
const count = await procurement.count({ activeOnly });
const response = apiResponse.successful(
"Catalog item count fetched successfully!",
{ count },
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
);
+80
View File
@@ -0,0 +1,80 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { procurement, CatalogFilterOpts } from "../../managers/procurement";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* /v1/procurement/items */
export default createRoute(
"get",
["/items"],
async (c) => {
const page = Number(c.req.query("page") ?? 1);
const rpp = Number(c.req.query("rpp") ?? 30);
const search = c.req.query("search") as string;
const includeInactive = c.req.query("includeInactive") === "true";
// Category / filter params
const category = c.req.query("category") as string | undefined;
const subcategory = c.req.query("subcategory") as string | undefined;
const group = c.req.query("group") as string | undefined;
const manufacturer = c.req.query("manufacturer") as string | undefined;
const ecosystem = c.req.query("ecosystem") as string | undefined;
const inStock = c.req.query("inStock") === "true" ? true : undefined;
const minPrice = c.req.query("minPrice")
? Number(c.req.query("minPrice"))
: undefined;
const maxPrice = c.req.query("maxPrice")
? Number(c.req.query("maxPrice"))
: undefined;
const filterOpts: CatalogFilterOpts = {
includeInactive,
category,
subcategory,
group,
manufacturer,
ecosystem,
inStock,
minPrice,
maxPrice,
};
const data = search
? await procurement.search(search, page, rpp, filterOpts)
: await procurement.fetchPages(page, rpp, filterOpts);
const totalRecords = search
? await procurement.countSearch(search, filterOpts)
: await procurement.count(filterOpts);
const gatedData = await Promise.all(
data.map((item) =>
processObjectValuePerms(
item.toJson(),
"obj.catalogItem",
c.get("user"),
),
),
);
const response = apiResponse.successful(
"Catalog items fetched successfully!",
gatedData,
{
pagination: {
previousPage: page <= 1 ? null : page - 1,
currentPage: page,
nextPage: page >= totalRecords / rpp ? null : page + 1,
totalPages: Math.ceil(totalRecords / rpp),
totalRecords,
listedRecords: rpp,
},
},
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
);
+32
View File
@@ -0,0 +1,32 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { procurement } from "../../managers/procurement";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
/* /v1/procurement/filters */
export default createRoute(
"get",
["/filters"],
async (c) => {
const category = c.req.query("category") as string | undefined;
const subcategory = c.req.query("subcategory") as string | undefined;
const includeInactive = c.req.query("includeInactive") === "true";
const filterOpts = { category, subcategory, includeInactive };
const [categories, subcategories, manufacturers] = await Promise.all([
procurement.fetchDistinctValues("category", filterOpts),
procurement.fetchDistinctValues("subcategory", filterOpts),
procurement.fetchDistinctValues("manufacturer", filterOpts),
]);
const response = apiResponse.successful(
"Available filter values fetched successfully!",
{ categories, subcategories, manufacturers },
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
);
+21
View File
@@ -0,0 +1,21 @@
import { default as fetchAll } from "./fetchAll";
import { default as fetch } from "./[id]/fetch";
import { default as refreshInventory } from "./[id]/refreshInventory";
import { default as link } from "./[id]/link";
import { default as unlink } from "./[id]/unlink";
import { default as fetchLinked } from "./[id]/fetchLinked";
import { default as count } from "./count";
import { default as categories } from "./categories";
import { default as filters } from "./filters";
export {
categories,
count,
fetch,
fetchAll,
fetchLinked,
filters,
link,
refreshInventory,
unlink,
};
+8 -1
View File
@@ -4,6 +4,7 @@ import { roles } from "../../managers/roles";
import { apiResponse } from "../../modules/api-utils/apiResponse"; import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* GET /v1/role/:identifier */ /* GET /v1/role/:identifier */
export default createRoute( export default createRoute(
@@ -15,9 +16,15 @@ export default createRoute(
const role = await roles.fetch(identifier); const role = await roles.fetch(identifier);
const gatedData = await processObjectValuePerms(
role.toJson({ viewPermissions: true }),
"obj.role",
c.get("user"),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"Role Fetched Successfully!", "Role Fetched Successfully!",
role.toJson({ viewPermissions: true }), gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+9 -2
View File
@@ -4,6 +4,7 @@ import { roles } from "../../managers/roles";
import { apiResponse } from "../../modules/api-utils/apiResponse"; import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* GET /v1/role */ /* GET /v1/role */
export default createRoute( export default createRoute(
@@ -13,13 +14,19 @@ export default createRoute(
async (c) => { async (c) => {
const allRoles = await roles.fetchAllRoles(); const allRoles = await roles.fetchAllRoles();
const rolesArray = allRoles.map((role) => const gatedData = await Promise.all(
allRoles.map((role) =>
processObjectValuePerms(
role.toJson({ viewPermissions: true }), role.toJson({ viewPermissions: true }),
"obj.role",
c.get("user"),
),
),
); );
const response = apiResponse.successful( const response = apiResponse.successful(
"Roles Fetched Successfully!", "Roles Fetched Successfully!",
rolesArray, gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+7 -2
View File
@@ -4,6 +4,7 @@ import { roles } from "../../managers/roles";
import { apiResponse } from "../../modules/api-utils/apiResponse"; import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* GET /v1/role/:identifier/users */ /* GET /v1/role/:identifier/users */
export default createRoute( export default createRoute(
@@ -16,11 +17,15 @@ export default createRoute(
const role = await roles.fetch(identifier); const role = await roles.fetch(identifier);
const users = role.getUsers(); const users = role.getUsers();
const usersArray = users.map((user) => user.toJson()); const gatedData = await Promise.all(
users.map((user) =>
processObjectValuePerms(user.toJson(), "obj.user", c.get("user")),
),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"Users Fetched Successfully!", "Users Fetched Successfully!",
usersArray, gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+7
View File
@@ -0,0 +1,7 @@
import { Hono } from "hono";
import * as procurementRoutes from "../procurement";
const procurementRouter = new Hono();
Object.values(procurementRoutes).map((r) => procurementRouter.route("/", r));
export default procurementRouter;
+7
View File
@@ -0,0 +1,7 @@
import { Hono } from "hono";
import * as salesRoutes from "../sales";
const salesRouter = new Hono();
Object.values(salesRoutes).map((r) => salesRouter.route("/", r));
export default salesRouter;
+69
View File
@@ -0,0 +1,69 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { opportunities } from "../../../managers/opportunities";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
import { z } from "zod";
const productItemSchema = z
.object({
catalogItem: z.object({ id: z.number().int().positive() }).optional(),
forecastDescription: z.string().optional(),
productDescription: z.string().optional(),
quantity: z.number().positive().optional(),
status: z.object({ id: z.number().int().positive() }).optional(),
productClass: z.string().optional(),
forecastType: z.string().optional(),
revenue: z.number().optional(),
cost: z.number().optional(),
includeFlag: z.boolean().optional(),
linkFlag: z.boolean().optional(),
recurringFlag: z.boolean().optional(),
taxableFlag: z.boolean().optional(),
recurringRevenue: z.number().optional(),
recurringCost: z.number().optional(),
cycles: z.number().int().min(0).optional(),
sequenceNumber: z.number().int().min(0).optional(),
})
.strict();
const addProductSchema = z.union([
productItemSchema,
z.array(productItemSchema).min(1, "At least one product is required"),
]);
/* POST /v1/sales/opportunities/:identifier/products */
export default createRoute(
"post",
["/opportunities/:identifier/products"],
async (c) => {
const identifier = c.req.param("identifier");
const body = await c.req.json();
const validated = addProductSchema.parse(body);
const inputItems = Array.isArray(validated) ? validated : [validated];
// Gate each submitted field against user permissions.
// Only fields the user has permission for are forwarded to ConnectWise.
const user = c.get("user");
const gatedItems = await Promise.all(
inputItems.map((item) =>
processObjectValuePerms(item, "sales.opportunity.product.field", user),
),
);
const item = await opportunities.fetchItem(identifier);
const created = await item.addProducts(gatedItems);
const isBatch = Array.isArray(body);
const response = apiResponse.created(
isBatch
? `${created.length} product(s) added to opportunity successfully!`
: "Product added to opportunity successfully!",
isBatch ? created.map((p) => p.toJson()) : created[0]!.toJson(),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.product.add"] }),
);
+25
View File
@@ -0,0 +1,25 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { opportunities } from "../../../managers/opportunities";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
/* GET /v1/sales/opportunities/:identifier/contacts */
export default createRoute(
"get",
["/opportunities/:identifier/contacts"],
async (c) => {
const identifier = c.req.param("identifier");
const item = await opportunities.fetchItem(identifier);
const data = await item.fetchContacts();
const response = apiResponse.successful(
"Opportunity contacts fetched successfully!",
data,
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
);
+47
View File
@@ -0,0 +1,47 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { opportunities } from "../../../managers/opportunities";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import { resolveMember } from "../../../modules/cw-utils/members/memberCache";
import { z } from "zod";
/* POST /v1/sales/opportunities/:identifier/notes */
export default createRoute(
"post",
["/opportunities/:identifier/notes"],
async (c) => {
const identifier = c.req.param("identifier");
const body = await c.req.json();
const schema = z.object({
text: z.string().min(1, "Note text is required"),
flagged: z.boolean().optional(),
});
const data = schema.parse(body);
const item = await opportunities.fetchItem(identifier);
const user = c.get("user");
const created = await item.addNote(data.text, user.login, {
flagged: data.flagged,
});
const response = apiResponse.created(
"Opportunity note created successfully!",
{
id: created.id,
text: created.text,
type: created.type
? { id: created.type.id, name: created.type.name }
: null,
flagged: created.flagged,
enteredBy: await resolveMember(created.enteredBy),
},
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.note.create"] }),
);
+33
View File
@@ -0,0 +1,33 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { opportunities } from "../../../managers/opportunities";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import GenericError from "../../../Errors/GenericError";
/* DELETE /v1/sales/opportunities/:identifier/notes/:noteId */
export default createRoute(
"delete",
["/opportunities/:identifier/notes/:noteId"],
async (c) => {
const identifier = c.req.param("identifier");
const noteId = Number(c.req.param("noteId"));
if (isNaN(noteId))
throw new GenericError({
status: 400,
name: "InvalidNoteId",
message: "Note ID must be a number",
});
const item = await opportunities.fetchItem(identifier);
await item.deleteNote(noteId);
const response = apiResponse.successful(
"Opportunity note deleted successfully!",
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.note.delete"] }),
);
+34
View File
@@ -0,0 +1,34 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { opportunities } from "../../../managers/opportunities";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
/* GET /v1/sales/opportunities/:identifier */
export default createRoute(
"get",
["/opportunities/:identifier"],
async (c) => {
const identifier = c.req.param("identifier");
const item = await opportunities.fetchItem(identifier);
// Eagerly load site data so toJson() includes full site info
await item.fetchSite();
const gatedData = await processObjectValuePerms(
item.toJson(),
"obj.opportunity",
c.get("user"),
);
const response = apiResponse.successful(
"Opportunity fetched successfully!",
gatedData,
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
);
+34
View File
@@ -0,0 +1,34 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { opportunities } from "../../../managers/opportunities";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import GenericError from "../../../Errors/GenericError";
/* GET /v1/sales/opportunities/:identifier/notes/:noteId */
export default createRoute(
"get",
["/opportunities/:identifier/notes/:noteId"],
async (c) => {
const identifier = c.req.param("identifier");
const noteId = Number(c.req.param("noteId"));
if (isNaN(noteId))
throw new GenericError({
status: 400,
name: "InvalidNoteId",
message: "Note ID must be a number",
});
const item = await opportunities.fetchItem(identifier);
const data = await item.fetchNote(noteId);
const response = apiResponse.successful(
"Opportunity note fetched successfully!",
data,
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
);
+25
View File
@@ -0,0 +1,25 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { opportunities } from "../../../managers/opportunities";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
/* GET /v1/sales/opportunities/:identifier/notes */
export default createRoute(
"get",
["/opportunities/:identifier/notes"],
async (c) => {
const identifier = c.req.param("identifier");
const item = await opportunities.fetchItem(identifier);
const data = await item.fetchNotes();
const response = apiResponse.successful(
"Opportunity notes fetched successfully!",
data,
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
);
+25
View File
@@ -0,0 +1,25 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { opportunities } from "../../../managers/opportunities";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
/* GET /v1/sales/opportunities/:identifier/products */
export default createRoute(
"get",
["/opportunities/:identifier/products"],
async (c) => {
const identifier = c.req.param("identifier");
const item = await opportunities.fetchItem(identifier);
const data = await item.fetchProducts();
const response = apiResponse.successful(
"Opportunity products fetched successfully!",
data.map((p) => p.toJson()),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
);
+25
View File
@@ -0,0 +1,25 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { opportunities } from "../../../managers/opportunities";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
/* POST /v1/sales/opportunities/:identifier/refresh */
export default createRoute(
"post",
["/opportunities/:identifier/refresh"],
async (c) => {
const identifier = c.req.param("identifier");
const item = await opportunities.fetchItem(identifier);
const refreshed = await item.refreshFromCW();
const response = apiResponse.successful(
"Opportunity refreshed from ConnectWise successfully!",
refreshed.toJson(),
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.refresh"] }),
);
+37
View File
@@ -0,0 +1,37 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { opportunities } from "../../../managers/opportunities";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import { z } from "zod";
/* PATCH /v1/sales/opportunities/:identifier/products/sequence */
export default createRoute(
"patch",
["/opportunities/:identifier/products/sequence"],
async (c) => {
const identifier = c.req.param("identifier");
const body = await c.req.json();
const schema = z.object({
orderedIds: z
.array(z.number().int().positive())
.min(1, "At least one forecast item ID is required"),
});
const { orderedIds } = schema.parse(body);
const item = await opportunities.fetchItem(identifier);
const updated = await item.resequenceProducts(orderedIds);
const response = apiResponse.successful(
"Product sequence updated successfully!",
{
products: updated.map((p) => p.toJson()),
},
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.product.update"] }),
);
+57
View File
@@ -0,0 +1,57 @@
import { createRoute } from "../../../modules/api-utils/createRoute";
import { opportunities } from "../../../managers/opportunities";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import GenericError from "../../../Errors/GenericError";
import { resolveMember } from "../../../modules/cw-utils/members/memberCache";
import { z } from "zod";
/* PATCH /v1/sales/opportunities/:identifier/notes/:noteId */
export default createRoute(
"patch",
["/opportunities/:identifier/notes/:noteId"],
async (c) => {
const identifier = c.req.param("identifier");
const noteId = Number(c.req.param("noteId"));
if (isNaN(noteId))
throw new GenericError({
status: 400,
name: "InvalidNoteId",
message: "Note ID must be a number",
});
const body = await c.req.json();
const schema = z
.object({
text: z.string().min(1).optional(),
flagged: z.boolean().optional(),
})
.refine((d) => d.text !== undefined || d.flagged !== undefined, {
message: "At least one of 'text' or 'flagged' must be provided",
});
const data = schema.parse(body);
const item = await opportunities.fetchItem(identifier);
const updated = await item.updateNote(noteId, data);
const response = apiResponse.successful(
"Opportunity note updated successfully!",
{
id: updated.id,
text: updated.text,
type: updated.type
? { id: updated.type.id, name: updated.type.name }
: null,
flagged: updated.flagged,
enteredBy: await resolveMember(updated.enteredBy),
},
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.note.update"] }),
);
+24
View File
@@ -0,0 +1,24 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { opportunities } from "../../managers/opportunities";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
/* GET /v1/sales/opportunities/count */
export default createRoute(
"get",
["/opportunities/count"],
async (c) => {
const openOnly = c.req.query("openOnly") === "true";
const count = await opportunities.count({ openOnly });
const response = apiResponse.successful(
"Opportunity count fetched successfully!",
{ count },
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.fetch.many"] }),
);
+54
View File
@@ -0,0 +1,54 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { opportunities } from "../../managers/opportunities";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* GET /v1/sales/opportunities */
export default createRoute(
"get",
["/opportunities"],
async (c) => {
const page = Number(c.req.query("page") ?? 1);
const rpp = Number(c.req.query("rpp") ?? 30);
const search = c.req.query("search") as string;
const includeClosed = c.req.query("includeClosed") === "true";
const data = search
? await opportunities.search(search, page, rpp, { includeClosed })
: await opportunities.fetchPages(page, rpp, { includeClosed });
const totalRecords = search
? await opportunities.searchCount(search, { includeClosed })
: await opportunities.count({ openOnly: !includeClosed });
const gatedData = await Promise.all(
data.map((item) =>
processObjectValuePerms(
item.toJson(),
"obj.opportunity",
c.get("user"),
),
),
);
const response = apiResponse.successful(
"Opportunities fetched successfully!",
gatedData,
{
pagination: {
previousPage: page <= 1 ? null : page - 1,
currentPage: page,
nextPage: page >= totalRecords / rpp ? null : page + 1,
totalPages: Math.ceil(totalRecords / rpp),
totalRecords,
listedRecords: rpp,
},
},
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.fetch.many"] }),
);
+20
View File
@@ -0,0 +1,20 @@
import { createRoute } from "../../modules/api-utils/createRoute";
import { apiResponse } from "../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../middleware/authorization";
import { QUOTE_STATUSES } from "../../types/QuoteStatuses";
/* GET /v1/sales/opportunity-types */
export default createRoute(
"get",
["/opportunity-types"],
async (c) => {
const response = apiResponse.successful(
"Opportunity Types Fetched Successfully!",
QUOTE_STATUSES,
);
return c.json(response, response.status as ContentfulStatusCode);
},
authMiddleware({ permissions: ["sales.opportunity.fetch.many"] }),
);
+31
View File
@@ -0,0 +1,31 @@
import { default as fetchAll } from "./fetchAll";
import { default as fetchOpportunityTypes } from "./fetchOpportunityTypes";
import { default as count } from "./count";
import { default as fetch } from "./[id]/fetch";
import { default as refresh } from "./[id]/refresh";
import { default as products } from "./[id]/products";
import { default as addProduct } from "./[id]/addProduct";
import { default as resequenceProducts } from "./[id]/resequenceProducts";
import { default as notes } from "./[id]/notes";
import { default as fetchNote } from "./[id]/fetchNote";
import { default as createNote } from "./[id]/createNote";
import { default as updateNote } from "./[id]/updateNote";
import { default as deleteNote } from "./[id]/deleteNote";
import { default as contacts } from "./[id]/contacts";
export {
addProduct,
count,
fetch,
fetchAll,
fetchOpportunityTypes,
products,
resequenceProducts,
notes,
fetchNote,
createNote,
updateNote,
deleteNote,
contacts,
refresh,
};
+2
View File
@@ -55,6 +55,8 @@ v1.route("/credential-type", require("./routers/credentialTypeRouter").default);
v1.route("/role", require("./routers/roleRouter").default); v1.route("/role", require("./routers/roleRouter").default);
v1.route("/permissions", require("./routers/permissionRouter").default); v1.route("/permissions", require("./routers/permissionRouter").default);
v1.route("/unifi", require("./routers/unifiRouter").default); v1.route("/unifi", require("./routers/unifiRouter").default);
v1.route("/procurement", require("./routers/procurementRouter").default);
v1.route("/sales", require("./routers/salesRouter").default);
app.route("/v1", v1); app.route("/v1", v1);
export default app; export default app;
+9 -1
View File
@@ -3,6 +3,7 @@ import { unifiSites } from "../../../managers/unifiSites";
import { apiResponse } from "../../../modules/api-utils/apiResponse"; import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization"; import { authMiddleware } from "../../middleware/authorization";
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
/* GET /v1/unifi/site/:id */ /* GET /v1/unifi/site/:id */
export default createRoute( export default createRoute(
@@ -10,9 +11,16 @@ export default createRoute(
["/site/:id"], ["/site/:id"],
async (c) => { async (c) => {
const site = await unifiSites.fetch(c.req.param("id")); const site = await unifiSites.fetch(c.req.param("id"));
const gatedData = await processObjectValuePerms(
site,
"obj.unifiSite",
c.get("user"),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"UniFi Site Fetched Successfully!", "UniFi Site Fetched Successfully!",
site, gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+9 -1
View File
@@ -3,6 +3,7 @@ import { unifiSites } from "../../../managers/unifiSites";
import { apiResponse } from "../../../modules/api-utils/apiResponse"; import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization"; import { authMiddleware } from "../../middleware/authorization";
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
/* GET /v1/unifi/sites */ /* GET /v1/unifi/sites */
export default createRoute( export default createRoute(
@@ -10,9 +11,16 @@ export default createRoute(
["/sites"], ["/sites"],
async (c) => { async (c) => {
const sites = await unifiSites.fetchAll(); const sites = await unifiSites.fetchAll();
const gatedData = await Promise.all(
sites.map((site) =>
processObjectValuePerms(site, "obj.unifiSite", c.get("user")),
),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"UniFi Sites Fetched Successfully!", "UniFi Sites Fetched Successfully!",
sites, gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+7 -3
View File
@@ -2,16 +2,20 @@ import { ContentfulStatusCode } from "hono/utils/http-status";
import { apiResponse } from "../../../modules/api-utils/apiResponse"; import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { createRoute } from "../../../modules/api-utils/createRoute"; import { createRoute } from "../../../modules/api-utils/createRoute";
import { authMiddleware } from "../../middleware/authorization"; import { authMiddleware } from "../../middleware/authorization";
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
// /v1/user/@me // /v1/user/@me
export default createRoute( export default createRoute(
"get", "get",
["/@me"], ["/@me"],
(c) => { async (c) => {
const response = apiResponse.successful( const gatedData = await processObjectValuePerms(
"Fetched user.",
c.get("user")?.toJson(), c.get("user")?.toJson(),
"obj.user",
c.get("user"),
); );
const response = apiResponse.successful("Fetched user.", gatedData);
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
authMiddleware({ scopes: ["user.read"] }), authMiddleware({ scopes: ["user.read"] }),
+8 -1
View File
@@ -4,6 +4,7 @@ import { createRoute } from "../../modules/api-utils/createRoute";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { users } from "../../managers/users"; import { users } from "../../managers/users";
import GenericError from "../../Errors/GenericError"; import GenericError from "../../Errors/GenericError";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* GET /v1/user/users/:identifier */ /* GET /v1/user/users/:identifier */
export default createRoute( export default createRoute(
@@ -21,9 +22,15 @@ export default createRoute(
status: 404, status: 404,
}); });
const gatedData = await processObjectValuePerms(
user.toJson(),
"obj.user",
c.get("user"),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"User Fetched Successfully!", "User Fetched Successfully!",
user.toJson(), gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+8 -2
View File
@@ -3,6 +3,7 @@ import { apiResponse } from "../../modules/api-utils/apiResponse";
import { createRoute } from "../../modules/api-utils/createRoute"; import { createRoute } from "../../modules/api-utils/createRoute";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { users } from "../../managers/users"; import { users } from "../../managers/users";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* GET /v1/user/users */ /* GET /v1/user/users */
export default createRoute( export default createRoute(
@@ -11,11 +12,16 @@ export default createRoute(
async (c) => { async (c) => {
const allUsers = await users.fetchAllUsers(); const allUsers = await users.fetchAllUsers();
const usersArray = allUsers.map((u) => u.toJson());
const gatedData = await Promise.all(
allUsers.map((u) =>
processObjectValuePerms(u.toJson(), "obj.user", c.get("user")),
),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"Users Fetched Successfully!", "Users Fetched Successfully!",
usersArray, gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+12 -2
View File
@@ -4,6 +4,7 @@ import { createRoute } from "../../modules/api-utils/createRoute";
import { authMiddleware } from "../middleware/authorization"; import { authMiddleware } from "../middleware/authorization";
import { users } from "../../managers/users"; import { users } from "../../managers/users";
import GenericError from "../../Errors/GenericError"; import GenericError from "../../Errors/GenericError";
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
/* GET /v1/user/users/:identifier/roles */ /* GET /v1/user/users/:identifier/roles */
export default createRoute( export default createRoute(
@@ -22,11 +23,20 @@ export default createRoute(
}); });
const roles = await user.fetchRoles(); const roles = await user.fetchRoles();
const rolesArray = roles.map((r) => r.toJson({ viewPermissions: true }));
const gatedData = await Promise.all(
roles.map((r) =>
processObjectValuePerms(
r.toJson({ viewPermissions: true }),
"obj.role",
c.get("user"),
),
),
);
const response = apiResponse.successful( const response = apiResponse.successful(
"User Roles Fetched Successfully!", "User Roles Fetched Successfully!",
rolesArray, gatedData,
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
}, },
+17 -36
View File
@@ -1,5 +1,4 @@
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import crypto from "crypto";
import { PrismaPg } from "@prisma/adapter-pg"; import { PrismaPg } from "@prisma/adapter-pg";
import { Prisma, PrismaClient } from "../generated/prisma/client"; import { Prisma, PrismaClient } from "../generated/prisma/client";
import * as msal from "@azure/msal-node"; import * as msal from "@azure/msal-node";
@@ -7,6 +6,7 @@ import { Server } from "socket.io";
import { Server as Engine } from "@socket.io/bun-engine"; import { Server as Engine } from "@socket.io/bun-engine";
import axios from "axios"; import axios from "axios";
import { UnifiClient } from "./modules/unifi-api/UnifiClient"; import { UnifiClient } from "./modules/unifi-api/UnifiClient";
import Redis from "ioredis";
const connectionString = `${process.env.DATABASE_URL}`; const connectionString = `${process.env.DATABASE_URL}`;
const adapter = new PrismaPg({ connectionString }); const adapter = new PrismaPg({ connectionString });
@@ -18,9 +18,15 @@ interface EnvKey {
// ENV CONSTANTS // ENV CONSTANTS
export const PORT = process.env.PORT; export const PORT = process.env.PORT;
export const API_BASE_URL =
process.env.API_BASE_URL || `http://localhost:${PORT || 3000}`;
export const prisma = new PrismaClient({ adapter }); export const prisma = new PrismaClient({ adapter });
// Redis Client
export const redis = new Redis(process.env.REDIS_URL!);
export const sessionDuration = 30 * 24 * 60 * 60000; export const sessionDuration = 30 * 24 * 60 * 60000;
export const accessTokenDuration = "10min"; export const accessTokenDuration = "10min";
export const refreshTokenDuration = "30d"; export const refreshTokenDuration = "30d";
@@ -29,46 +35,21 @@ const isProduction = process.env.NODE_ENV === "production";
const readKeyFile = (path: string) => readFileSync(path).toString(); const readKeyFile = (path: string) => readFileSync(path).toString();
/** export const accessTokenPrivateKey = isProduction
* Convert a PKCS#1 PEM key to PKCS#8 PEM format.
* The compiled Bun binary on Ubuntu uses an OpenSSL that doesn't auto-detect PKCS#1 format,
* so we normalize all keys to PKCS#8 at load time.
*/
const toPkcs8Private = (pem: string) =>
crypto
.createPrivateKey({ key: pem, format: "pem", type: "pkcs1" })
.export({ type: "pkcs8", format: "pem" }) as string;
const toPkcs8Public = (pem: string) =>
crypto
.createPublicKey({ key: pem, format: "pem", type: "pkcs1" })
.export({ type: "spki", format: "pem" }) as string;
export const accessTokenPrivateKey = toPkcs8Private(
isProduction
? process.env.ACCESS_TOKEN_PRIVATE_KEY! ? process.env.ACCESS_TOKEN_PRIVATE_KEY!
: readKeyFile(`.accessToken.key`), : readKeyFile(`.accessToken.key`);
); export const refreshTokenPrivateKey = isProduction
export const refreshTokenPrivateKey = toPkcs8Private(
isProduction
? process.env.REFRESH_TOKEN_PRIVATE_KEY! ? process.env.REFRESH_TOKEN_PRIVATE_KEY!
: readKeyFile(`.refreshToken.key`), : readKeyFile(`.refreshToken.key`);
); export const permissionsPrivateKey = isProduction
export const permissionsPrivateKey = toPkcs8Private(
isProduction
? process.env.PERMISSIONS_PRIVATE_KEY! ? process.env.PERMISSIONS_PRIVATE_KEY!
: readKeyFile(`.permissions.key`), : readKeyFile(`.permissions.key`);
); export const secureValuesPrivateKey = isProduction
export const secureValuesPrivateKey = toPkcs8Private(
isProduction
? process.env.SECURE_VALUES_PRIVATE_KEY! ? process.env.SECURE_VALUES_PRIVATE_KEY!
: readKeyFile(`.secureValues.key`), : readKeyFile(`.secureValues.key`);
); export const secureValuesPublicKey = isProduction
export const secureValuesPublicKey = toPkcs8Public(
isProduction
? process.env.SECURE_VALUES_PUBLIC_KEY! ? process.env.SECURE_VALUES_PUBLIC_KEY!
: readKeyFile(`public-keys/.secureValues.pub`), : readKeyFile(`public-keys/.secureValues.pub`);
);
// Microsoft Auth Constants // Microsoft Auth Constants
const msalConfig: msal.Configuration = { const msalConfig: msal.Configuration = {
+252
View File
@@ -0,0 +1,252 @@
import {
CWActivity,
CWActivityCustomField,
CWPatchOperation,
CWCreateActivity,
} from "../modules/cw-utils/activities/activity.types";
import { activityCw } from "../modules/cw-utils/activities/activities";
import { fetchActivity } from "../modules/cw-utils/activities/fetchActivity";
/**
* Activity Controller
*
* Domain model class that encapsulates a ConnectWise Activity entity.
* Activities are not persisted locally all data is sourced directly
* from the ConnectWise API.
*/
export class ActivityController {
public readonly cwActivityId: number;
public name: string;
public notes: string | null;
public typeName: string | null;
public typeCwId: number | null;
public statusName: string | null;
public statusCwId: number | null;
public companyCwId: number | null;
public companyName: string | null;
public companyIdentifier: string | null;
public contactCwId: number | null;
public contactName: string | null;
public phoneNumber: string | null;
public email: string | null;
public opportunityCwId: number | null;
public opportunityName: string | null;
public ticketCwId: number | null;
public ticketName: string | null;
public agreementCwId: number | null;
public agreementName: string | null;
public campaignCwId: number | null;
public campaignName: string | null;
public assignToCwId: number | null;
public assignToName: string | null;
public assignToIdentifier: string | null;
public scheduleStatusCwId: number | null;
public scheduleStatusName: string | null;
public reminderCwId: number | null;
public reminderName: string | null;
public whereCwId: number | null;
public whereName: string | null;
public dateStart: Date | null;
public dateEnd: Date | null;
public notifyFlag: boolean;
public currencyCwId: number | null;
public currencyName: string | null;
public mobileGuid: string | null;
public customFields: CWActivityCustomField[];
public cwLastUpdated: Date | null;
public cwDateEntered: Date | null;
public cwEnteredBy: string | null;
public cwUpdatedBy: string | null;
constructor(data: CWActivity) {
this.cwActivityId = data.id;
this.name = data.name;
this.notes = data.notes ?? null;
this.typeName = data.type?.name ?? null;
this.typeCwId = data.type?.id ?? null;
this.statusName = data.status?.name ?? null;
this.statusCwId = data.status?.id ?? null;
this.companyCwId = data.company?.id ?? null;
this.companyName = data.company?.name ?? null;
this.companyIdentifier = data.company?.identifier ?? null;
this.contactCwId = data.contact?.id ?? null;
this.contactName = data.contact?.name ?? null;
this.phoneNumber = data.phoneNumber ?? null;
this.email = data.email ?? null;
this.opportunityCwId = data.opportunity?.id ?? null;
this.opportunityName = data.opportunity?.name ?? null;
this.ticketCwId = data.ticket?.id ?? null;
this.ticketName = data.ticket?.name ?? null;
this.agreementCwId = data.agreement?.id ?? null;
this.agreementName = data.agreement?.name ?? null;
this.campaignCwId = data.campaign?.id ?? null;
this.campaignName = data.campaign?.name ?? null;
this.assignToCwId = data.assignTo?.id ?? null;
this.assignToName = data.assignTo?.name ?? null;
this.assignToIdentifier = data.assignTo?.identifier ?? null;
this.scheduleStatusCwId = data.scheduleStatus?.id ?? null;
this.scheduleStatusName = data.scheduleStatus?.name ?? null;
this.reminderCwId = data.reminder?.id ?? null;
this.reminderName = data.reminder?.name ?? null;
this.whereCwId = data.where?.id ?? null;
this.whereName = data.where?.name ?? null;
this.dateStart = data.dateStart ? new Date(data.dateStart) : null;
this.dateEnd = data.dateEnd ? new Date(data.dateEnd) : null;
this.notifyFlag = data.notifyFlag ?? false;
this.currencyCwId = data.currency?.id ?? null;
this.currencyName = data.currency?.name ?? null;
this.mobileGuid = data.mobileGuid ?? null;
this.customFields = data.customFields ?? [];
this.cwLastUpdated = data._info?.lastUpdated
? new Date(data._info.lastUpdated)
: null;
this.cwDateEntered = data._info?.dateEntered
? new Date(data._info.dateEntered)
: null;
this.cwEnteredBy = data._info?.enteredBy ?? null;
this.cwUpdatedBy = data._info?.updatedBy ?? null;
}
/**
* Refresh from ConnectWise
*
* Fetches the latest activity data from CW and returns
* a new ActivityController instance with updated state.
*/
public async refreshFromCW(): Promise<ActivityController> {
const cwData = await fetchActivity(this.cwActivityId);
return new ActivityController(cwData);
}
/**
* Fetch raw CW data
*
* Returns the raw ConnectWise activity object.
*/
public async fetchCwData(): Promise<CWActivity> {
return fetchActivity(this.cwActivityId);
}
/**
* Update in ConnectWise
*
* Applies JSON Patch operations to this activity in ConnectWise
* and returns a new controller with the updated data.
*/
public async update(
operations: CWPatchOperation[],
): Promise<ActivityController> {
const updated = await activityCw.update(this.cwActivityId, operations);
return new ActivityController(updated);
}
/**
* Delete from ConnectWise
*
* Deletes this activity in ConnectWise.
*/
public async delete(): Promise<void> {
await activityCw.delete(this.cwActivityId);
}
/**
* Create Activity (static factory)
*
* Creates a new activity in ConnectWise and returns a controller instance.
*/
public static async create(
data: CWCreateActivity,
): Promise<ActivityController> {
const created = await activityCw.create(data);
return new ActivityController(created);
}
/**
* To JSON
*
* Serializes the activity into a safe, API-friendly object.
*/
public toJson(): Record<string, any> {
return {
cwActivityId: this.cwActivityId,
name: this.name,
notes: this.notes,
type: this.typeCwId ? { id: this.typeCwId, name: this.typeName } : null,
status: this.statusCwId
? { id: this.statusCwId, name: this.statusName }
: null,
company: this.companyCwId
? {
id: this.companyCwId,
identifier: this.companyIdentifier,
name: this.companyName,
}
: null,
contact: this.contactCwId
? { id: this.contactCwId, name: this.contactName }
: null,
phoneNumber: this.phoneNumber,
email: this.email,
opportunity: this.opportunityCwId
? { id: this.opportunityCwId, name: this.opportunityName }
: null,
ticket: this.ticketCwId
? { id: this.ticketCwId, name: this.ticketName }
: null,
agreement: this.agreementCwId
? { id: this.agreementCwId, name: this.agreementName }
: null,
campaign: this.campaignCwId
? { id: this.campaignCwId, name: this.campaignName }
: null,
assignTo: this.assignToCwId
? {
id: this.assignToCwId,
identifier: this.assignToIdentifier,
name: this.assignToName,
}
: null,
scheduleStatus: this.scheduleStatusCwId
? { id: this.scheduleStatusCwId, name: this.scheduleStatusName }
: null,
reminder: this.reminderCwId
? { id: this.reminderCwId, name: this.reminderName }
: null,
where: this.whereCwId
? { id: this.whereCwId, name: this.whereName }
: null,
dateStart: this.dateStart,
dateEnd: this.dateEnd,
notifyFlag: this.notifyFlag,
currency: this.currencyCwId
? { id: this.currencyCwId, name: this.currencyName }
: null,
mobileGuid: this.mobileGuid,
customFields: this.customFields,
cwLastUpdated: this.cwLastUpdated,
cwDateEntered: this.cwDateEntered,
cwEnteredBy: this.cwEnteredBy,
cwUpdatedBy: this.cwUpdatedBy,
};
}
}
+231
View File
@@ -0,0 +1,231 @@
import { CatalogItem } from "../../generated/prisma/client";
import { prisma } from "../constants";
import { catalogCw } from "../modules/cw-utils/procurement/catalog";
import { CatalogItem as CWCatalogItem } from "../modules/cw-utils/procurement/catalog.types";
import GenericError from "../Errors/GenericError";
/**
* Catalog Item Controller
*
* This class encapsulates a catalog item entity and provides domain methods
* for accessing, refreshing, and serializing catalog item data. It bridges
* the internal database representation with ConnectWise catalog data.
*/
export class CatalogItemController {
public readonly id: string;
public name: string;
public description: string | null;
public customerDescription: string | null;
public internalNotes: string | null;
public readonly cwCatalogId: number;
public readonly identifier: string | null;
public category: string | null;
public categoryCwId: number | null;
public subcategory: string | null;
public subcategoryCwId: number | null;
public manufacturer: string | null;
public manufactureCwId: number | null;
public partNumber: string | null;
public vendorName: string | null;
public vendorSku: string | null;
public vendorCwId: number | null;
public price: number;
public cost: number;
public inactive: boolean;
public salesTaxable: boolean;
public onHand: number;
public cwLastUpdated: Date | null;
private _linkedItems: CatalogItemController[];
public readonly createdAt: Date;
public updatedAt: Date;
constructor(
itemData: CatalogItem & {
linkedItems?: CatalogItem[];
},
) {
this.id = itemData.id;
this.name = itemData.name;
this.description = itemData.description;
this.customerDescription = itemData.customerDescription;
this.internalNotes = itemData.internalNotes;
this.cwCatalogId = itemData.cwCatalogId;
this.identifier = itemData.identifier;
this.category = itemData.category;
this.categoryCwId = itemData.categoryCwId;
this.subcategory = itemData.subcategory;
this.subcategoryCwId = itemData.subcategoryCwId;
this.manufacturer = itemData.manufacturer;
this.manufactureCwId = itemData.manufactureCwId;
this.partNumber = itemData.partNumber;
this.vendorName = itemData.vendorName;
this.vendorSku = itemData.vendorSku;
this.vendorCwId = itemData.vendorCwId;
this.price = itemData.price;
this.cost = itemData.cost;
this.inactive = itemData.inactive;
this.salesTaxable = itemData.salesTaxable;
this.onHand = itemData.onHand;
this.cwLastUpdated = itemData.cwLastUpdated;
this.createdAt = itemData.createdAt;
this.updatedAt = itemData.updatedAt;
this._linkedItems = (itemData.linkedItems ?? []).map(
(linked) => new CatalogItemController(linked),
);
}
/**
* Refresh Inventory
*
* Fetches the latest on-hand inventory count from ConnectWise
* and updates both the controller state and the database.
*
* @returns {Promise<CatalogItemController>} - The updated controller
*/
public async refreshInventory(): Promise<CatalogItemController> {
const onHand = await catalogCw.fetchInventoryOnHand(this.cwCatalogId);
if (onHand !== this.onHand) {
await prisma.catalogItem.update({
where: { id: this.id },
data: { onHand },
});
this.onHand = onHand;
}
return this;
}
/**
* Fetch Linked Items
*
* Returns the linked catalog items as an array of controllers.
*
* @returns {CatalogItemController[]} - Array of linked item controllers
*/
public getLinkedItems(): CatalogItemController[] {
return this._linkedItems;
}
/**
* Link Item
*
* Links another catalog item to this item. The relationship is bidirectional
* via the Prisma implicit many-to-many.
*
* @param targetId - The internal ID of the catalog item to link
* @returns {Promise<CatalogItemController>} - The updated controller
*/
public async linkItem(targetId: string): Promise<CatalogItemController> {
if (targetId === this.id) {
throw new GenericError({
message: "Cannot link a catalog item to itself",
name: "InvalidLinkTarget",
cause: `Item '${this.id}' cannot be linked to itself`,
status: 400,
});
}
const target = await prisma.catalogItem.findFirst({
where: { id: targetId },
});
if (!target) {
throw new GenericError({
message: "Target catalog item not found",
name: "CatalogItemNotFound",
cause: `No catalog item exists with ID '${targetId}'`,
status: 404,
});
}
const updated = await prisma.catalogItem.update({
where: { id: this.id },
data: {
linkedItems: { connect: { id: targetId } },
},
include: { linkedItems: true },
});
this._linkedItems = (updated.linkedItems ?? []).map(
(linked) => new CatalogItemController(linked),
);
return this;
}
/**
* Unlink Item
*
* Removes the link between this catalog item and another.
*
* @param targetId - The internal ID of the catalog item to unlink
* @returns {Promise<CatalogItemController>} - The updated controller
*/
public async unlinkItem(targetId: string): Promise<CatalogItemController> {
const updated = await prisma.catalogItem.update({
where: { id: this.id },
data: {
linkedItems: { disconnect: { id: targetId } },
},
include: { linkedItems: true },
});
this._linkedItems = (updated.linkedItems ?? []).map(
(linked) => new CatalogItemController(linked),
);
return this;
}
/**
* To JSON
*
* Serializes the catalog item into a safe, API-friendly object.
*
* @param opts - Options to control output
* @returns - A JSON-safe representation of the catalog item
*/
public toJson(opts?: { includeLinkedItems?: boolean }): Record<string, any> {
return {
id: this.id,
cwCatalogId: this.cwCatalogId,
identifier: this.identifier,
name: this.name,
description: this.description,
customerDescription: this.customerDescription,
internalNotes: this.internalNotes,
category: this.category,
categoryCwId: this.categoryCwId,
subcategory: this.subcategory,
subcategoryCwId: this.subcategoryCwId,
manufacturer: this.manufacturer,
manufactureCwId: this.manufactureCwId,
partNumber: this.partNumber,
vendorName: this.vendorName,
vendorSku: this.vendorSku,
vendorCwId: this.vendorCwId,
price: this.price,
cost: this.cost,
inactive: this.inactive,
salesTaxable: this.salesTaxable,
onHand: this.onHand,
cwLastUpdated: this.cwLastUpdated,
linkedItems: opts?.includeLinkedItems
? this._linkedItems.map((item) => item.toJson())
: undefined,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
};
}
}
+76 -12
View File
@@ -1,7 +1,13 @@
import { Company } from "../../generated/prisma/client"; import { Company } from "../../generated/prisma/client";
import { connectWiseApi } from "../constants";
import { fetchCwCompanyById } from "../modules/cw-utils/fetchCompany"; import { fetchCwCompanyById } from "../modules/cw-utils/fetchCompany";
import { fetchCompanyConfigurations } from "../modules/cw-utils/configurations/fetchCompanyConfigurations"; import { fetchCompanyConfigurations } from "../modules/cw-utils/configurations/fetchCompanyConfigurations";
import { updateCwInternalCompany } from "../modules/cw-utils/updateCompany"; import { updateCwInternalCompany } from "../modules/cw-utils/updateCompany";
import {
fetchCompanySites,
fetchCompanySite,
serializeCwSite,
} from "../modules/cw-utils/sites/companySites";
import { Company as CWCompany, Contact } from "../types/ConnectWiseTypes"; import { Company as CWCompany, Contact } from "../types/ConnectWiseTypes";
/** /**
@@ -16,9 +22,9 @@ export class CompanyController {
public name: string; public name: string;
public readonly cw_Identifier: string; public readonly cw_Identifier: string;
public readonly cw_CompanyId: number; public readonly cw_CompanyId: number;
public readonly cw_Data?: { public cw_Data?: {
company: CWCompany; company: CWCompany;
defaultContact: Contact; defaultContact: Contact | null;
allContacts: Contact[]; allContacts: Contact[];
}; };
@@ -30,6 +36,38 @@ export class CompanyController {
this.cw_Data = cwData; this.cw_Data = cwData;
} }
/**
* Hydrate CW Data
*
* Fetches and populates the full ConnectWise company data
* (company, default contact, all contacts) if not already loaded.
*
* @returns {ThisType}
*/
public async hydrateCwData() {
if (this.cw_Data) return this;
const cwCompany = await fetchCwCompanyById(this.cw_CompanyId);
if (!cwCompany) return this;
const contactHref = cwCompany.defaultContact?._info?.contact_href;
const defaultContactData = contactHref
? await connectWiseApi.get(contactHref)
: undefined;
const allContactsData = await connectWiseApi.get(
`${cwCompany._info.contacts_href}&pageSize=1000`,
);
this.cw_Data = {
company: cwCompany,
defaultContact: defaultContactData?.data ?? null,
allContacts: allContactsData.data,
};
return this;
}
/** /**
* Refresh Internal Company Data from ConnectWise * Refresh Internal Company Data from ConnectWise
* *
@@ -71,6 +109,30 @@ export class CompanyController {
return data; return data;
} }
/**
* Fetch Company Sites
*
* Retrieves all sites for this company from ConnectWise
* and returns them as serialized site objects.
*/
public async fetchSites() {
const sites = await fetchCompanySites(this.cw_CompanyId);
return sites.map(serializeCwSite);
}
/**
* Fetch Company Site by ID
*
* Retrieves a single site by its ConnectWise site ID
* and returns a serialized site object.
*
* @param cwSiteId - The ConnectWise site ID
*/
public async fetchSite(cwSiteId: number) {
const site = await fetchCompanySite(this.cw_CompanyId, cwSiteId);
return serializeCwSite(site);
}
public toJson(opts?: { public toJson(opts?: {
includeAddress: boolean; includeAddress: boolean;
includePrimaryContact: boolean; includePrimaryContact: boolean;
@@ -96,23 +158,25 @@ export class CompanyController {
}, },
primaryContact: !opts?.includePrimaryContact primaryContact: !opts?.includePrimaryContact
? undefined ? undefined
: { : this.cw_Data?.defaultContact
firstName: this.cw_Data?.defaultContact.firstName, ? {
lastName: this.cw_Data?.defaultContact.lastName, firstName: this.cw_Data.defaultContact.firstName,
cwId: this.cw_Data?.defaultContact.id, lastName: this.cw_Data.defaultContact.lastName,
inactive: this.cw_Data?.defaultContact.inactiveFlag, cwId: this.cw_Data.defaultContact.id,
title: this.cw_Data?.defaultContact.title, inactive: this.cw_Data.defaultContact.inactiveFlag,
phone: this.cw_Data?.defaultContact.defaultPhoneNbr, title: this.cw_Data.defaultContact.title,
phone: this.cw_Data.defaultContact.defaultPhoneNbr,
email: (() => { email: (() => {
if (!this.cw_Data?.defaultContact.communicationItems) if (!this.cw_Data?.defaultContact?.communicationItems)
return null; return null;
return ( return (
this.cw_Data?.defaultContact.communicationItems.find( this.cw_Data.defaultContact.communicationItems.find(
(v) => v.type.name === "Email", (v) => v.type.name === "Email",
)?.value ?? null )?.value ?? null
); );
})(), })(),
}, }
: null,
allContacts: !opts?.includeAllContacts allContacts: !opts?.includeAllContacts
? undefined ? undefined
: this.cw_Data?.allContacts.map((contact) => ({ : this.cw_Data?.allContacts.map((contact) => ({
@@ -0,0 +1,226 @@
import { CWForecastItem } from "../modules/cw-utils/opportunities/opportunity.types";
/**
* Forecast Product Controller
*
* Domain model class that encapsulates a ConnectWise Forecast Item (product/
* revenue line item on an opportunity). Forecast products are not persisted
* locally all data is sourced directly from the ConnectWise API.
*/
export class ForecastProductController {
public readonly cwForecastId: number;
public forecastDescription: string;
public opportunityCwId: number | null;
public opportunityName: string | null;
public quantity: number;
public statusCwId: number | null;
public statusName: string | null;
public catalogItemCwId: number | null;
public catalogItemIdentifier: string | null;
public productDescription: string;
public productClass: string;
public forecastType: string;
public revenue: number;
public cost: number;
public margin: number;
public percentage: number;
public includeFlag: boolean;
public linkFlag: boolean;
public recurringFlag: boolean;
public taxableFlag: boolean;
public recurringRevenue: number;
public recurringCost: number;
public cycles: number;
public sequenceNumber: number;
public subNumber: number;
public quoteWerksQuantity: number;
public cwLastUpdated: Date | null;
public cwUpdatedBy: string | null;
// Cancellation data (from procurement products endpoint)
public cancelledFlag: boolean;
public quantityCancelled: number;
public cancelledReason: string | null;
public cancelledBy: number | null;
public cancelledDate: Date | null;
// Internal inventory data (from local CatalogItem database)
public onHand: number | null;
public inStock: boolean | null;
constructor(data: CWForecastItem) {
this.cwForecastId = data.id;
this.forecastDescription = data.forecastDescription;
this.opportunityCwId = data.opportunity?.id ?? null;
this.opportunityName = data.opportunity?.name ?? null;
this.quantity = data.quantity;
this.statusCwId = data.status?.id ?? null;
this.statusName = data.status?.name ?? null;
this.catalogItemCwId = data.catalogItem?.id ?? null;
this.catalogItemIdentifier = data.catalogItem?.identifier ?? null;
this.productDescription = data.productDescription;
this.productClass = data.productClass;
this.forecastType = data.forecastType;
this.revenue = data.revenue;
this.cost = data.cost;
this.margin = data.margin;
this.percentage = data.percentage;
this.includeFlag = data.includeFlag ?? false;
this.linkFlag = data.linkFlag ?? false;
this.recurringFlag = data.recurringFlag ?? false;
this.taxableFlag = data.taxableFlag ?? false;
this.recurringRevenue = data.recurringRevenue ?? 0;
this.recurringCost = data.recurringCost ?? 0;
this.cycles = data.cycles ?? 0;
this.sequenceNumber = data.sequenceNumber ?? 0;
this.subNumber = data.subNumber ?? 0;
this.quoteWerksQuantity = data.quoteWerksQuantity ?? 0;
this.cwLastUpdated = data._info?.lastUpdated
? new Date(data._info.lastUpdated)
: null;
this.cwUpdatedBy = data._info?.updatedBy ?? null;
// Cancellation defaults — enriched later via applyCancellationData()
this.cancelledFlag = false;
this.quantityCancelled = 0;
this.cancelledReason = null;
this.cancelledBy = null;
this.cancelledDate = null;
// Inventory defaults — enriched later via applyInventoryData()
this.onHand = null;
this.inStock = null;
}
/**
* Apply Cancellation Data
*
* Enriches this forecast product with cancellation data from the
* procurement products endpoint.
*/
public applyCancellationData(data: {
cancelledFlag?: boolean;
quantityCancelled?: number;
cancelledReason?: string;
cancelledBy?: number;
cancelledDate?: string;
}): void {
this.cancelledFlag = data.cancelledFlag ?? false;
this.quantityCancelled = data.quantityCancelled ?? 0;
this.cancelledReason = data.cancelledReason ?? null;
this.cancelledBy = data.cancelledBy ?? null;
this.cancelledDate = data.cancelledDate
? new Date(data.cancelledDate)
: null;
}
/**
* Apply Inventory Data
*
* Enriches this forecast product with internal inventory data from
* the local CatalogItem database.
*/
public applyInventoryData(data: { onHand: number }): void {
this.onHand = data.onHand;
this.inStock = data.onHand > 0;
}
/**
* Profit
*
* Returns the calculated profit (revenue - cost).
*/
public get profit(): number {
return this.revenue - this.cost;
}
/**
* Cancelled
*
* Returns true if the forecast item has been cancelled (fully or partially).
*/
public get cancelled(): boolean {
return this.cancelledFlag;
}
/**
* Cancellation Type
*
* Returns the type of cancellation:
* - `"full"` all units have been cancelled (`quantityCancelled >= quantity`)
* - `"partial"` some units cancelled but not all
* - `null` not cancelled
*/
public get cancellationType(): "full" | "partial" | null {
if (!this.cancelledFlag || this.quantityCancelled <= 0) return null;
return this.quantityCancelled >= this.quantity ? "full" : "partial";
}
/**
* To JSON
*
* Serializes the forecast product into a safe, API-friendly object.
*/
public toJson(): Record<string, any> {
return {
id: this.cwForecastId,
forecastDescription: this.forecastDescription,
opportunity: this.opportunityCwId
? { id: this.opportunityCwId, name: this.opportunityName }
: null,
quantity: this.quantity,
status: this.statusCwId
? { id: this.statusCwId, name: this.statusName }
: null,
cancelled: this.cancelled,
cancellationType: this.cancellationType,
quantityCancelled: this.quantityCancelled,
cancelledReason: this.cancelledReason,
cancelledDate: this.cancelledDate,
catalogItem: this.catalogItemCwId
? { id: this.catalogItemCwId, identifier: this.catalogItemIdentifier }
: null,
productDescription: this.productDescription,
productClass: this.productClass,
forecastType: this.forecastType,
revenue: this.revenue,
cost: this.cost,
margin: this.margin,
profit: this.profit,
percentage: this.percentage,
includeFlag: this.includeFlag,
linkFlag: this.linkFlag,
recurringFlag: this.recurringFlag,
taxableFlag: this.taxableFlag,
recurringRevenue: this.recurringRevenue,
recurringCost: this.recurringCost,
cycles: this.cycles,
sequenceNumber: this.sequenceNumber,
subNumber: this.subNumber,
cwLastUpdated: this.cwLastUpdated,
cwUpdatedBy: this.cwUpdatedBy,
onHand: this.onHand,
inStock: this.inStock,
};
}
}
+801
View File
@@ -0,0 +1,801 @@
import { Company, Opportunity } from "../../generated/prisma/client";
import { prisma } from "../constants";
import { CompanyController } from "./CompanyController";
import { ActivityController } from "./ActivityController";
import { fetchOpportunity } from "../modules/cw-utils/opportunities/fetchOpportunity";
import { opportunityCw } from "../modules/cw-utils/opportunities/opportunities";
import { activityCw } from "../modules/cw-utils/activities/activities";
import {
fetchCompanySite,
serializeCwSite,
} from "../modules/cw-utils/sites/companySites";
import {
CWCustomField,
CWForecastItemCreate,
CWOpportunity,
CWOpportunityNote,
} from "../modules/cw-utils/opportunities/opportunity.types";
import { resolveMember } from "../modules/cw-utils/members/memberCache";
import { ForecastProductController } from "./ForecastProductController";
import GenericError from "../Errors/GenericError";
/**
* Opportunity Controller
*
* Domain model class that encapsulates an Opportunity entity and provides
* methods for accessing, refreshing from ConnectWise, and serializing
* opportunity data.
*/
export class OpportunityController {
public readonly id: string;
public readonly cwOpportunityId: number;
public name: string;
public notes: string | null;
public typeName: string | null;
public typeCwId: number | null;
public stageName: string | null;
public stageCwId: number | null;
public statusName: string | null;
public statusCwId: number | null;
public priorityName: string | null;
public priorityCwId: number | null;
public ratingName: string | null;
public ratingCwId: number | null;
public source: string | null;
public campaignName: string | null;
public campaignCwId: number | null;
public primarySalesRepName: string | null;
public primarySalesRepIdentifier: string | null;
public primarySalesRepCwId: number | null;
public secondarySalesRepName: string | null;
public secondarySalesRepIdentifier: string | null;
public secondarySalesRepCwId: number | null;
public companyCwId: number | null;
public companyName: string | null;
public contactCwId: number | null;
public contactName: string | null;
public siteCwId: number | null;
public siteName: string | null;
public customerPO: string | null;
public totalSalesTax: number;
public locationName: string | null;
public locationCwId: number | null;
public departmentName: string | null;
public departmentCwId: number | null;
public expectedCloseDate: Date | null;
public pipelineChangeDate: Date | null;
public dateBecameLead: Date | null;
public closedDate: Date | null;
public closedFlag: boolean;
public closedByName: string | null;
public closedByCwId: number | null;
public companyId: string | null;
public cwLastUpdated: Date | null;
// Local product display order — array of CW forecast item IDs.
// When non-empty, fetchProducts() uses this instead of CW sequenceNumber.
public productSequence: number[];
public readonly createdAt: Date;
public updatedAt: Date;
private _company: CompanyController | null = null;
private _siteData: ReturnType<typeof serializeCwSite> | null = null;
private _customFields: CWCustomField[] | null = null;
private _activities: ActivityController[] | null = null;
constructor(
data: Opportunity & { company?: Company | null },
opts?: {
company?: CompanyController;
customFields?: CWCustomField[];
activities?: ActivityController[];
},
) {
this.id = data.id;
this.cwOpportunityId = data.cwOpportunityId;
this.name = data.name;
this.notes = data.notes;
this.typeName = data.typeName;
this.typeCwId = data.typeCwId;
this.stageName = data.stageName;
this.stageCwId = data.stageCwId;
this.statusName = data.statusName;
this.statusCwId = data.statusCwId;
this.priorityName = data.priorityName;
this.priorityCwId = data.priorityCwId;
this.ratingName = data.ratingName;
this.ratingCwId = data.ratingCwId;
this.source = data.source;
this.campaignName = data.campaignName;
this.campaignCwId = data.campaignCwId;
this.primarySalesRepName = data.primarySalesRepName;
this.primarySalesRepIdentifier = data.primarySalesRepIdentifier;
this.primarySalesRepCwId = data.primarySalesRepCwId;
this.secondarySalesRepName = data.secondarySalesRepName;
this.secondarySalesRepIdentifier = data.secondarySalesRepIdentifier;
this.secondarySalesRepCwId = data.secondarySalesRepCwId;
this.companyCwId = data.companyCwId;
this.companyName = data.companyName;
this.contactCwId = data.contactCwId;
this.contactName = data.contactName;
this.siteCwId = data.siteCwId;
this.siteName = data.siteName;
this.customerPO = data.customerPO;
this.totalSalesTax = data.totalSalesTax;
this.locationName = data.locationName;
this.locationCwId = data.locationCwId;
this.departmentName = data.departmentName;
this.departmentCwId = data.departmentCwId;
this.expectedCloseDate = data.expectedCloseDate;
this.pipelineChangeDate = data.pipelineChangeDate;
this.dateBecameLead = data.dateBecameLead;
this.closedDate = data.closedDate;
this.closedFlag = data.closedFlag;
this.closedByName = data.closedByName;
this.closedByCwId = data.closedByCwId;
this.companyId = data.companyId;
this.cwLastUpdated = data.cwLastUpdated;
this.productSequence = data.productSequence;
this.createdAt = data.createdAt;
this.updatedAt = data.updatedAt;
this._company =
opts?.company ??
(data.company ? new CompanyController(data.company) : null);
this._customFields = opts?.customFields ?? null;
this._activities = opts?.activities ?? null;
}
/**
* Fetch Company
*
* Lazily loads the associated CompanyController from the database
* if not already loaded via the Prisma include.
*
* @returns {Promise<CompanyController | null>}
*/
public async fetchCompany(): Promise<CompanyController | null> {
if (this._company) {
await this._company.hydrateCwData();
return this._company;
}
if (!this.companyId) return null;
const companyData = await prisma.company.findUnique({
where: { id: this.companyId },
});
if (!companyData) return null;
this._company = new CompanyController(companyData);
await this._company.hydrateCwData();
return this._company;
}
/**
* Refresh from ConnectWise
*
* Fetches the latest opportunity data from CW and updates
* the local database record and controller state.
*/
public async refreshFromCW(): Promise<OpportunityController> {
const cwData = await fetchOpportunity(this.cwOpportunityId);
const mapped = OpportunityController.mapCwToDb(cwData);
const updated = await prisma.opportunity.update({
where: { id: this.id },
data: mapped,
include: { company: true },
});
return new OpportunityController(updated);
}
/**
* Fetch raw CW data
*
* Returns the raw ConnectWise opportunity object without updating the DB.
*/
public async fetchCwData(): Promise<CWOpportunity> {
return fetchOpportunity(this.cwOpportunityId);
}
/**
* Map CW Opportunity Prisma create/update payload
*
* Static helper used by both the controller and the refresh sync.
*/
public static mapCwToDb(item: CWOpportunity) {
return {
name: item.name,
notes: item.notes ?? null,
typeName: item.type?.name ?? null,
typeCwId: item.type?.id ?? null,
stageName: item.stage?.name ?? null,
stageCwId: item.stage?.id ?? null,
statusName: item.status?.name ?? null,
statusCwId: item.status?.id ?? null,
priorityName: item.priority?.name ?? null,
priorityCwId: item.priority?.id ?? null,
ratingName: item.rating?.name ?? null,
ratingCwId: item.rating?.id ?? null,
source: item.source ?? null,
campaignName: item.campaign?.name ?? null,
campaignCwId: item.campaign?.id ?? null,
primarySalesRepName: item.primarySalesRep?.name ?? null,
primarySalesRepIdentifier: item.primarySalesRep?.identifier ?? null,
primarySalesRepCwId: item.primarySalesRep?.id ?? null,
secondarySalesRepName: item.secondarySalesRep?.name ?? null,
secondarySalesRepIdentifier: item.secondarySalesRep?.identifier ?? null,
secondarySalesRepCwId: item.secondarySalesRep?.id ?? null,
companyCwId: item.company?.id ?? null,
companyName: item.company?.name ?? null,
contactCwId: item.contact?.id ?? null,
contactName: item.contact?.name ?? null,
siteCwId: item.site?.id ?? null,
siteName: item.site?.name ?? null,
customerPO: item.customerPO ?? null,
totalSalesTax: item.totalSalesTax ?? 0,
locationName: item.location?.name ?? null,
locationCwId: item.location?.id ?? null,
departmentName: item.department?.name ?? null,
departmentCwId: item.department?.id ?? null,
expectedCloseDate: item.expectedCloseDate
? new Date(item.expectedCloseDate)
: null,
pipelineChangeDate: item.pipelineChangeDate
? new Date(item.pipelineChangeDate)
: null,
dateBecameLead: item.dateBecameLead
? new Date(item.dateBecameLead)
: null,
closedDate: item.closedDate ? new Date(item.closedDate) : null,
closedFlag: item.closedFlag ?? false,
closedByName: item.closedBy?.name ?? null,
closedByCwId: item.closedBy?.id ?? null,
cwLastUpdated: item._info?.lastUpdated
? new Date(item._info.lastUpdated)
: new Date(),
};
}
/**
* Fetch Site
*
* Fetches the full site details (address, phone, flags) from ConnectWise
* for the site associated with this opportunity.
* Requires both companyCwId and siteCwId to be set.
*
* @returns Serialized site object or null
*/
public async fetchSite() {
if (this._siteData) return this._siteData;
if (!this.companyCwId || !this.siteCwId) return null;
const cwSite = await fetchCompanySite(this.companyCwId, this.siteCwId);
this._siteData = serializeCwSite(cwSite);
return this._siteData;
}
/**
* Fetch Contacts
*
* Fetches contacts associated with this opportunity from ConnectWise
* and returns a serialized array.
*/
public async fetchContacts() {
const contacts = await opportunityCw.fetchContacts(this.cwOpportunityId);
return contacts.map((ct) => ({
id: ct.id,
contact: ct.contact ? { id: ct.contact.id, name: ct.contact.name } : null,
company: ct.company
? {
id: ct.company.id,
identifier: ct.company.identifier,
name: ct.company.name,
}
: null,
role: ct.role ? { id: ct.role.id, name: ct.role.name } : null,
notes: ct.notes,
referralFlag: ct.referralFlag,
}));
}
/**
* Fetch Notes
*
* Fetches notes associated with this opportunity from ConnectWise
* and returns a serialized array.
*/
public async fetchNotes() {
const notes = await opportunityCw.fetchNotes(this.cwOpportunityId);
return Promise.all(
notes.map(async (n) => ({
id: n.id,
text: n.text,
type: n.type ? { id: n.type.id, name: n.type.name } : null,
flagged: n.flagged,
dateEntered: n._info?.lastUpdated
? new Date(n._info.lastUpdated)
: null,
enteredBy: await resolveMember(n.enteredBy),
})),
);
}
/**
* Fetch Single Note
*
* Fetches a single note by its ID from ConnectWise.
*
* @param noteId - The CW note ID
*/
public async fetchNote(noteId: number) {
const note = await opportunityCw.fetchNote(this.cwOpportunityId, noteId);
return {
id: note.id,
text: note.text,
type: note.type ? { id: note.type.id, name: note.type.name } : null,
flagged: note.flagged,
enteredBy: await resolveMember(note.enteredBy),
};
}
/**
* Fetch Activities
*
* Fetches activities associated with this opportunity from ConnectWise
* and returns an array of ActivityController instances.
* Results are cached after the first call.
*/
public async fetchActivities(): Promise<ActivityController[]> {
if (this._activities) return this._activities;
const collection = await activityCw.fetchByOpportunity(
this.cwOpportunityId,
);
this._activities = collection.map((item) => new ActivityController(item));
return this._activities;
}
/**
* Fetch Products
*
* Fetches products (forecast/revenue items) for this opportunity from
* ConnectWise and returns ForecastProductController instances.
*/
public async fetchProducts(): Promise<ForecastProductController[]> {
const [forecast, procProducts] = await Promise.all([
opportunityCw.fetchProducts(this.cwOpportunityId),
opportunityCw.fetchProcurementProducts(this.cwOpportunityId),
]);
// Build a map of forecastDetailId → procurement product cancellation data
const cancellationMap = new Map<number, Record<string, unknown>>();
for (const pp of procProducts) {
const forecastDetailId = pp.forecastDetailId as number | undefined;
if (forecastDetailId) {
cancellationMap.set(forecastDetailId, pp);
}
}
// Apply local ordering if productSequence is set, otherwise fall back
// to CW sequenceNumber.
const forecastItems = forecast.forecastItems ?? [];
let ordered: typeof forecastItems;
if (this.productSequence.length > 0) {
const itemById = new Map(forecastItems.map((fi) => [fi.id, fi]));
// Items in the specified order first, then any new items not yet sequenced
const sequenced = this.productSequence
.map((id) => itemById.get(id))
.filter((fi): fi is NonNullable<typeof fi> => fi !== undefined);
const sequencedIds = new Set(this.productSequence);
const unsequenced = forecastItems
.filter((fi) => !sequencedIds.has(fi.id))
.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
ordered = [...sequenced, ...unsequenced];
} else {
ordered = [...forecastItems].sort(
(a, b) => a.sequenceNumber - b.sequenceNumber,
);
}
const controllers = ordered.map((item) => {
const ctrl = new ForecastProductController(item);
const procData = cancellationMap.get(item.id);
if (procData) {
ctrl.applyCancellationData(procData as any);
}
return ctrl;
});
// Enrich with internal inventory data from local CatalogItem DB
const catalogCwIds = controllers
.map((c) => c.catalogItemCwId)
.filter((id): id is number => id !== null);
if (catalogCwIds.length > 0) {
const catalogItems = await prisma.catalogItem.findMany({
where: { cwCatalogId: { in: catalogCwIds } },
select: { cwCatalogId: true, onHand: true },
});
const inventoryMap = new Map(
catalogItems.map((ci) => [ci.cwCatalogId, ci]),
);
for (const ctrl of controllers) {
const inv = ctrl.catalogItemCwId
? inventoryMap.get(ctrl.catalogItemCwId)
: undefined;
if (inv) ctrl.applyInventoryData(inv);
}
}
return controllers;
}
// ---------------------------------------------------------------------------
// Opportunity Activity / Workflow Methods
// ---------------------------------------------------------------------------
/**
* Set Internal Review
*
* The quote is ready to be reviewed before it is ready to be sent.
*/
public async setInternalReview(): Promise<void> {
// TODO: implement
}
/**
* Set Internal Approved
*
* The quote has been approved and is ready to be sent out.
*/
public async setInternalApproved(): Promise<void> {
// TODO: implement
}
/**
* Set Quote Sent
*
* The quote has been sent to the customer.
*/
public async setQuoteSent(): Promise<void> {
// TODO: implement
}
/**
* Set Quote Confirmed
*
* The quote has been received by the customer.
*/
public async setQuoteConfirmed(): Promise<void> {
// TODO: implement
}
/**
* Set Revision Needed
*
* The quote needs to be revised and is set to stage revision.
*/
public async setRevisionNeeded(): Promise<void> {
// TODO: implement
}
/**
* Set Finalized
*
* Locks any non-admins from modifying the quote, indicating
* this is the final iteration of the quote.
*/
public async setFinalized(): Promise<void> {
// TODO: implement
}
/**
* Convert
*
* Converts the quote to a ticket and updates all necessary fields.
*/
public async convert(): Promise<void> {
// TODO: implement
}
/**
* Add Time
*
* Adds time to an activity on this opportunity.
*
* @param activityId - The CW activity ID to add time to
* @param user - The user identifier adding time
*/
public async addTime(activityId: number, user: string): Promise<void> {
// TODO: implement
}
/**
* Update Product
*
* Updates an existing product/line item on this opportunity via PATCH.
*
* @param forecastItemId - The CW forecast item ID to update
* @param data - Key/value pairs to patch
*/
public async updateProduct(
forecastItemId: number,
data: Record<string, unknown>,
): Promise<ForecastProductController> {
try {
const updated = await opportunityCw.updateProduct(
this.cwOpportunityId,
forecastItemId,
data,
);
return new ForecastProductController(updated);
} catch (err: any) {
console.error(
`[updateProduct] Failed to patch forecast item ${forecastItemId} on opportunity ${this.cwOpportunityId}`,
JSON.stringify(
{
data,
status: err?.response?.status,
statusText: err?.response?.statusText,
responseData: err?.response?.data,
message: err?.message,
},
null,
2,
),
);
throw err;
}
}
/**
* Resequence Products
*
* Stores the desired display order of forecast item IDs locally in
* the database. No CW API calls are made CW item IDs are stable
* and ordering is applied when `fetchProducts()` is called.
*
* @param orderedIds - Forecast item IDs in the desired display order
*/
public async resequenceProducts(
orderedIds: number[],
): Promise<ForecastProductController[]> {
// Validate all IDs exist in CW
const forecast = await opportunityCw.fetchProducts(this.cwOpportunityId);
const existingIds = new Set(
(forecast.forecastItems ?? []).map((fi) => fi.id),
);
for (const id of orderedIds) {
if (!existingIds.has(id)) {
throw new GenericError({
status: 404,
name: "ForecastItemNotFound",
message: `Forecast item ${id} not found on opportunity ${this.cwOpportunityId}`,
});
}
}
// Persist the sequence locally
await prisma.opportunity.update({
where: { id: this.id },
data: { productSequence: orderedIds },
});
this.productSequence = orderedIds;
// Return items in the new order
return this.fetchProducts();
}
/**
* Add Products
*
* Adds one or more products/line items to this opportunity via the
* ConnectWise forecast endpoint. The caller passes only the fields
* the user is permitted to set (already filtered by field-level
* permission gating in the route handler).
*
* Accepts a single item or an array of items.
*/
public async addProducts(
data: CWForecastItemCreate | CWForecastItemCreate[],
): Promise<ForecastProductController[]> {
try {
const created = await opportunityCw.createProducts(
this.cwOpportunityId,
data,
);
return created.map((item) => new ForecastProductController(item));
} catch (err: any) {
console.error(
`[addProducts] Failed to create forecast item(s) on opportunity ${this.cwOpportunityId}`,
JSON.stringify(
{
data,
status: err?.response?.status,
statusText: err?.response?.statusText,
responseData: err?.response?.data,
message: err?.message,
},
null,
2,
),
);
throw new GenericError({
status: err?.response?.status ?? 500,
name: "AddProductFailed",
message:
err?.response?.data?.message ??
"Failed to add product(s) to opportunity",
cause: err?.message,
});
}
}
/**
* Add Note
*
* Creates a new note on this opportunity in ConnectWise.
*
* @param note - The note text to add
* @param user - The user identifier adding the note
* @param opts - Optional flags
*/
public async addNote(
note: string,
user: string,
opts?: { flagged?: boolean },
): Promise<CWOpportunityNote> {
const created = await opportunityCw.createNote(this.cwOpportunityId, {
text: note,
flagged: opts?.flagged ?? false,
});
return created;
}
/**
* Update Note
*
* Updates an existing note on this opportunity in ConnectWise.
*
* @param noteId - The CW note ID to update
* @param data - The fields to update
*/
public async updateNote(
noteId: number,
data: { text?: string; flagged?: boolean },
): Promise<CWOpportunityNote> {
const updated = await opportunityCw.updateNote(
this.cwOpportunityId,
noteId,
data,
);
return updated;
}
/**
* Delete Note
*
* Deletes a note from this opportunity in ConnectWise.
*
* @param noteId - The CW note ID to delete
*/
public async deleteNote(noteId: number): Promise<void> {
await opportunityCw.deleteNote(this.cwOpportunityId, noteId);
}
/**
* To JSON
*
* Serializes the opportunity into a safe, API-friendly object.
*/
public toJson(): Record<string, any> {
return {
id: this.id,
cwOpportunityId: this.cwOpportunityId,
name: this.name,
notes: this.notes,
type: this.typeCwId ? { id: this.typeCwId, name: this.typeName } : null,
stage: this.stageCwId
? { id: this.stageCwId, name: this.stageName }
: null,
status: this.statusCwId
? { id: this.statusCwId, name: this.statusName }
: null,
priority: this.priorityCwId
? { id: this.priorityCwId, name: this.priorityName }
: null,
rating: this.ratingCwId
? { id: this.ratingCwId, name: this.ratingName }
: null,
source: this.source,
campaign: this.campaignCwId
? { id: this.campaignCwId, name: this.campaignName }
: null,
primarySalesRep: this.primarySalesRepCwId
? {
id: this.primarySalesRepCwId,
identifier: this.primarySalesRepIdentifier,
name: this.primarySalesRepName,
}
: null,
secondarySalesRep: this.secondarySalesRepCwId
? {
id: this.secondarySalesRepCwId,
identifier: this.secondarySalesRepIdentifier,
name: this.secondarySalesRepName,
}
: null,
company: this._company
? this._company.toJson({
includeAllContacts: true,
includeAddress: true,
includePrimaryContact: false,
})
: this.companyCwId
? { id: this.companyCwId, name: this.companyName }
: null,
contact: this.contactCwId
? { id: this.contactCwId, name: this.contactName }
: null,
site: this._siteData
? this._siteData
: this.siteCwId
? { id: this.siteCwId, name: this.siteName }
: null,
customerPO: this.customerPO,
totalSalesTax: this.totalSalesTax,
location: this.locationCwId
? { id: this.locationCwId, name: this.locationName }
: null,
department: this.departmentCwId
? { id: this.departmentCwId, name: this.departmentName }
: null,
expectedCloseDate: this.expectedCloseDate,
pipelineChangeDate: this.pipelineChangeDate,
dateBecameLead: this.dateBecameLead,
closedDate: this.closedDate,
closedFlag: this.closedFlag,
closedBy: this.closedByCwId
? { id: this.closedByCwId, name: this.closedByName }
: null,
companyId: this.companyId,
cwLastUpdated: this.cwLastUpdated,
productSequence: this.productSequence,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
customFields: this._customFields ?? [],
activities: this._activities?.map((a) => a.toJson()) ?? [],
};
}
}
+54 -1
View File
@@ -19,6 +19,7 @@ export default class UserController {
public login: string; public login: string;
public email: string; public email: string;
public image: string | null; public image: string | null;
public cwIdentifier: string | null;
private _roles: Collection<string, Role>; private _roles: Collection<string, Role>;
private _permissions: string | null; private _permissions: string | null;
@@ -31,6 +32,7 @@ export default class UserController {
this.login = userdata.login; this.login = userdata.login;
this.email = userdata.email; this.email = userdata.email;
this.image = userdata.image; this.image = userdata.image;
this.cwIdentifier = userdata.cwIdentifier ?? null;
this.updatedAt = userdata.updatedAt; this.updatedAt = userdata.updatedAt;
this.createdAt = userdata.createdAt; this.createdAt = userdata.createdAt;
this._permissions = userdata.permissions ?? null; this._permissions = userdata.permissions ?? null;
@@ -57,6 +59,7 @@ export default class UserController {
this.login = userdata.login; this.login = userdata.login;
this.email = userdata.email; this.email = userdata.email;
this.image = userdata.image; this.image = userdata.image;
this.cwIdentifier = userdata.cwIdentifier ?? null;
this.updatedAt = userdata.updatedAt; this.updatedAt = userdata.updatedAt;
this.createdAt = userdata.createdAt; this.createdAt = userdata.createdAt;
} }
@@ -178,6 +181,46 @@ export default class UserController {
return decoded.permissions; return decoded.permissions;
} }
/**
* Read Role Permissions
*
* Verifies and decodes a role permissions JWT and returns the permission nodes.
* Returns an empty array if verification fails.
*
* @param role - Role record containing the signed permissions token
* @returns {string[]} The role permission nodes
*/
private _readRolePermissions(role: Role): string[] {
try {
const decoded = jwt.verify(role.permissions, permissionsPrivateKey, {
algorithms: ["RS256"],
issuer: "roles",
subject: role.id,
}) as DecodedPermissionsBlock;
return decoded.permissions;
} catch {
return [];
}
}
/**
* Read All Permissions
*
* Aggregates the user's direct permissions and all permissions from their assigned roles
* into a single deduplicated array.
*
* @returns {Promise<string[]>} Combined array of all permission nodes
*/
public async readAllPermissions(): Promise<string[]> {
const directPermissions = this.readPermissions();
const rolePermissions = this._roles
.map((role) => this._readRolePermissions(role))
.flatMap((permissions) => permissions);
return [...new Set([...directPermissions, ...rolePermissions])];
}
/** /**
* Fetch Roles * Fetch Roles
* *
@@ -262,9 +305,19 @@ export default class UserController {
: this._roles.size > 0 : this._roles.size > 0
? this._roles.map((v) => v.moniker) ? this._roles.map((v) => v.moniker)
: undefined, : undefined,
permissions: opts?.safeReturn ? undefined : this.readPermissions(), permissions: opts?.safeReturn
? undefined
: (() => {
const directPermissions = this.readPermissions();
const rolePermissions = this._roles
.map((role) => this._readRolePermissions(role))
.flatMap((permissions) => permissions);
return [...new Set([...directPermissions, ...rolePermissions])];
})(),
login: opts?.safeReturn ? undefined : this.login, login: opts?.safeReturn ? undefined : this.login,
email: opts?.safeReturn ? undefined : this.email, email: opts?.safeReturn ? undefined : this.email,
cwIdentifier: opts?.safeReturn ? undefined : this.cwIdentifier,
image: this.image, image: this.image,
createdAt: this.createdAt, createdAt: this.createdAt,
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
+125 -26
View File
@@ -1,41 +1,44 @@
import { refresh } from "./api/auth"; import { refresh } from "./api/auth";
import app from "./api/server"; import app from "./api/server";
import { engine, PORT, unifi, unifiPassword, unifiUsername } from "./constants"; import {
engine,
PORT,
prisma,
unifi,
unifiPassword,
unifiUsername,
} from "./constants";
import { unifiSites } from "./managers/unifiSites"; import { unifiSites } from "./managers/unifiSites";
import { refreshCompanies } from "./modules/cw-utils/refreshCompanies"; import { refreshCompanies } from "./modules/cw-utils/refreshCompanies";
import { refreshCatalog } from "./modules/cw-utils/procurement/refreshCatalog"; import { refreshCatalog } from "./modules/cw-utils/procurement/refreshCatalog";
import { refreshInventory } from "./modules/cw-utils/procurement/refreshInventory"; import { refreshInventory } from "./modules/cw-utils/procurement/refreshInventory";
import { refreshOpportunities } from "./modules/cw-utils/opportunities/refreshOpportunities";
import { refreshCwIdentifiers } from "./modules/cw-utils/members/refreshCwIdentifiers";
import { userDefinedFieldsCw } from "./modules/cw-utils/userDefinedFields";
import { events, setupEventDebugger } from "./modules/globalEvents"; import { events, setupEventDebugger } from "./modules/globalEvents";
import { signPermissions } from "./modules/permission-utils/signPermissions";
import { RoleController } from "./controllers/RoleController";
import cuid from "cuid";
// Setup global event debugger in non-production environments // Setup global event debugger in non-production environments
if (Bun.env.NODE_ENV == "development") setupEventDebugger(); if (Bun.env.NODE_ENV == "development") setupEventDebugger();
// Refresh the internal list of companies every minute // Helper to run a startup sync safely — failures are logged but never crash the process.
await refreshCompanies(); const safeStartup = async (label: string, fn: () => Promise<void>) => {
setInterval(() => { try {
return refreshCompanies(); await fn();
}, 60 * 1000); } catch (err) {
console.error(
// Refresh the internal catalog every minute `[startup] ${label} failed — will retry on next interval`,
await refreshCatalog(); err,
setInterval(() => { );
return refreshCatalog(); }
}, 60 * 1000); };
// Refresh inventory on hand every 2 minutes
await refreshInventory();
setInterval(
() => {
return refreshInventory();
},
2 * 60 * 1000,
);
await unifiSites.syncSites();
setInterval(() => {
return unifiSites.syncSites();
}, 60 * 1000);
// ---------------------------------------------------------------------------
// Start the HTTP server FIRST so the pod is reachable immediately.
// All data-sync tasks run afterwards and are non-blocking.
// ---------------------------------------------------------------------------
Bun.serve({ Bun.serve({
port: PORT, port: PORT,
websocket: engine.handler().websocket, websocket: engine.handler().websocket,
@@ -49,3 +52,99 @@ Bun.serve({
return app.fetch(req, server); return app.fetch(req, server);
}, },
}); });
console.log(`[startup] Server listening on port ${PORT}`);
// ---------------------------------------------------------------------------
// Background initialisation — none of this blocks the server.
// ---------------------------------------------------------------------------
// Ensure administrator role exists
await safeStartup("ensureAdminRole", async () => {
const existingAdmin = await prisma.role.findFirst({
where: { moniker: "administrator" },
include: { users: { include: { roles: true } } },
});
if (!existingAdmin) {
const id = cuid();
const created = await prisma.role.create({
data: {
id,
moniker: "administrator",
title: "Admin",
permissions: signPermissions({
issuer: "roles",
subject: id,
permissions: ["*"],
}),
},
include: { users: { include: { roles: true } } },
});
events.emit("role:created", new RoleController(created));
}
});
// Refresh the internal list of companies every minute
await safeStartup("refreshCompanies", refreshCompanies);
setInterval(() => {
return refreshCompanies().catch((err) =>
console.error("[interval] refreshCompanies failed", err),
);
}, 60 * 1000);
// Refresh the internal catalog every minute
await safeStartup("refreshCatalog", refreshCatalog);
setInterval(() => {
return refreshCatalog().catch((err) =>
console.error("[interval] refreshCatalog failed", err),
);
}, 60 * 1000);
// Refresh inventory on hand every 2 minutes
await safeStartup("refreshInventory", refreshInventory);
setInterval(
() => {
return refreshInventory().catch((err) =>
console.error("[interval] refreshInventory failed", err),
);
},
2 * 60 * 1000,
);
// Refresh opportunities every minute
await safeStartup("refreshOpportunities", refreshOpportunities);
setInterval(() => {
return refreshOpportunities().catch((err) =>
console.error("[interval] refreshOpportunities failed", err),
);
}, 60 * 1000);
// Refresh User Defined Fields every 5 minutes
await safeStartup("refreshUDFs", () => userDefinedFieldsCw.refresh());
setInterval(
() => {
return userDefinedFieldsCw
.refresh()
.catch((err) => console.error("[interval] refreshUDFs failed", err));
},
5 * 60 * 1000,
);
// Refresh CW identifiers for all users every 30 minutes
await safeStartup("refreshCwIdentifiers", refreshCwIdentifiers);
setInterval(
() => {
return refreshCwIdentifiers().catch((err) =>
console.error("[interval] refreshCwIdentifiers failed", err),
);
},
30 * 60 * 1000,
);
await safeStartup("syncSites", () => unifiSites.syncSites());
setInterval(() => {
return unifiSites
.syncSites()
.catch((err) => console.error("[interval] syncSites failed", err));
}, 60 * 1000);
+211
View File
@@ -0,0 +1,211 @@
import { ActivityController } from "../controllers/ActivityController";
import { connectWiseApi } from "../constants";
import GenericError from "../Errors/GenericError";
import { activityCw } from "../modules/cw-utils/activities/activities";
import {
CWCreateActivity,
CWPatchOperation,
} from "../modules/cw-utils/activities/activity.types";
export const activities = {
/**
* Fetch Activity
*
* Fetch a single activity by its ConnectWise activity ID
* and return an ActivityController instance.
*
* @param cwActivityId - The ConnectWise activity ID
* @returns {Promise<ActivityController>}
*/
async fetchItem(cwActivityId: number): Promise<ActivityController> {
try {
const cwData = await activityCw.fetch(cwActivityId);
return new ActivityController(cwData);
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({
name: "FetchActivityError",
message: `Failed to fetch activity ${cwActivityId}`,
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: (error as any).status ?? 502,
});
}
},
/**
* Fetch All Activities (Paginated)
*
* Fetches activities from ConnectWise with optional conditions and pagination.
*
* @param page - Page number (1-based)
* @param rpp - Records per page
* @param conditions - Optional CW conditions string for filtering
* @returns {Promise<ActivityController[]>}
*/
async fetchPages(
page: number,
rpp: number,
conditions?: string,
): Promise<ActivityController[]> {
try {
const pageNum = Math.max(page, 1);
const conditionsParam = conditions
? `&conditions=${encodeURIComponent(conditions)}`
: "";
const response = await connectWiseApi.get(
`/sales/activities?page=${pageNum}&pageSize=${rpp}${conditionsParam}`,
);
const items = response.data;
return items.map((item: any) => new ActivityController(item));
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({
name: "FetchActivitiesError",
message: "Failed to fetch activities from ConnectWise",
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
},
/**
* Fetch Activities by Company
*
* Fetches all activities for a company by its ConnectWise company ID.
*
* @param cwCompanyId - The ConnectWise company ID
* @returns {Promise<ActivityController[]>}
*/
async fetchByCompany(cwCompanyId: number): Promise<ActivityController[]> {
try {
const collection = await activityCw.fetchByCompany(cwCompanyId);
return collection.map((item) => new ActivityController(item));
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({
name: "FetchCompanyActivitiesError",
message: `Failed to fetch activities for company ${cwCompanyId}`,
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
},
/**
* Fetch Activities by Opportunity
*
* Fetches all activities for an opportunity by its ConnectWise opportunity ID.
*
* @param cwOpportunityId - The ConnectWise opportunity ID
* @returns {Promise<ActivityController[]>}
*/
async fetchByOpportunity(
cwOpportunityId: number,
): Promise<ActivityController[]> {
try {
const collection = await activityCw.fetchByOpportunity(cwOpportunityId);
return collection.map((item) => new ActivityController(item));
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({
name: "FetchOpportunityActivitiesError",
message: `Failed to fetch activities for opportunity ${cwOpportunityId}`,
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
},
/**
* Create Activity
*
* Creates a new activity in ConnectWise and returns an ActivityController.
*
* @param data - The activity data to create
* @returns {Promise<ActivityController>}
*/
async create(data: CWCreateActivity): Promise<ActivityController> {
try {
return await ActivityController.create(data);
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({
name: "CreateActivityError",
message: "Failed to create activity in ConnectWise",
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
},
/**
* Update Activity
*
* Updates an existing activity in ConnectWise using JSON Patch operations
* and returns an updated ActivityController.
*
* @param cwActivityId - The ConnectWise activity ID to update
* @param operations - Array of JSON Patch operations to apply
* @returns {Promise<ActivityController>}
*/
async update(
cwActivityId: number,
operations: CWPatchOperation[],
): Promise<ActivityController> {
try {
const updated = await activityCw.update(cwActivityId, operations);
return new ActivityController(updated);
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({
name: "UpdateActivityError",
message: `Failed to update activity ${cwActivityId}`,
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
},
/**
* Delete Activity
*
* Deletes an activity from ConnectWise.
*
* @param cwActivityId - The ConnectWise activity ID to delete
*/
async delete(cwActivityId: number): Promise<void> {
try {
await activityCw.delete(cwActivityId);
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({
name: "DeleteActivityError",
message: `Failed to delete activity ${cwActivityId}`,
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
},
/**
* Count Activities
*
* Returns the total number of activities, optionally filtered.
*
* @param conditions - Optional CW conditions string for filtering
* @returns {Promise<number>}
*/
async count(conditions?: string): Promise<number> {
try {
return await activityCw.countItems(conditions);
} catch (error) {
const errBody = (error as any).response?.data || error;
throw new GenericError({
name: "CountActivitiesError",
message: "Failed to count activities in ConnectWise",
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
},
};
+7 -4
View File
@@ -15,16 +15,19 @@ export const companies = {
const freshCwData: { data: Company } = await connectWiseApi.get( const freshCwData: { data: Company } = await connectWiseApi.get(
`/company/companies/${search.cw_CompanyId}`, `/company/companies/${search.cw_CompanyId}`,
); );
const defaultContactData = await connectWiseApi.get(
(freshCwData.data as Company).defaultContact._info.contact_href, const contactHref = freshCwData.data.defaultContact?._info?.contact_href;
); const defaultContactData = contactHref
? await connectWiseApi.get(contactHref)
: undefined;
const allContactsData = await connectWiseApi.get( const allContactsData = await connectWiseApi.get(
`${freshCwData.data._info.contacts_href}&pageSize=1000`, `${freshCwData.data._info.contacts_href}&pageSize=1000`,
); );
return new CompanyController(search, { return new CompanyController(search, {
company: freshCwData.data, company: freshCwData.data,
defaultContact: defaultContactData.data, defaultContact: defaultContactData?.data ?? null,
allContacts: allContactsData.data, allContacts: allContactsData.data,
}); });
}, },
+272
View File
@@ -0,0 +1,272 @@
import { Company } from "../../generated/prisma/client";
import { prisma } from "../constants";
import { ActivityController } from "../controllers/ActivityController";
import { CompanyController } from "../controllers/CompanyController";
import { OpportunityController } from "../controllers/OpportunityController";
import GenericError from "../Errors/GenericError";
import { activityCw } from "../modules/cw-utils/activities/activities";
import { opportunityCw } from "../modules/cw-utils/opportunities/opportunities";
/**
* Build a CompanyController with hydrated CW data from a Prisma Company record.
*/
async function buildCompanyController(
company: Company,
): Promise<CompanyController> {
const ctrl = new CompanyController(company);
await ctrl.hydrateCwData();
return ctrl;
}
/**
* Fetch ActivityController[] for an opportunity from ConnectWise.
*/
async function buildActivities(
cwOpportunityId: number,
): Promise<ActivityController[]> {
const collection = await activityCw.fetchByOpportunity(cwOpportunityId);
return collection.map((item) => new ActivityController(item));
}
export const opportunities = {
/**
* Fetch Opportunity
*
* Fetch an opportunity by its internal ID or ConnectWise opportunity ID
* and return an OpportunityController instance.
*
* @param identifier - The internal ID (string) or CW opportunity ID (number)
* @returns {Promise<OpportunityController>}
*/
async fetchItem(identifier: string | number): Promise<OpportunityController> {
const isNumeric =
typeof identifier === "number" || /^\d+$/.test(String(identifier));
// Look up the existing DB record to get the cwOpportunityId
const existing = await prisma.opportunity.findFirst({
where: isNumeric
? { cwOpportunityId: Number(identifier) }
: { id: identifier as string },
select: { id: true, cwOpportunityId: true },
});
if (!existing) {
throw new GenericError({
message: "Opportunity not found",
name: "OpportunityNotFound",
cause: `No opportunity exists with identifier '${identifier}'`,
status: 404,
});
}
// Fetch fresh data from ConnectWise
const cwData = await opportunityCw.fetch(existing.cwOpportunityId);
// Map and update the DB record
const mapped = OpportunityController.mapCwToDb(cwData);
// Resolve internal company link
const companyId = cwData.company?.id
? ((
await prisma.company.findFirst({
where: { cw_CompanyId: cwData.company.id },
select: { id: true },
})
)?.id ?? null)
: null;
const updated = await prisma.opportunity.update({
where: { id: existing.id },
data: { ...mapped, companyId },
include: { company: true },
});
const activities = await buildActivities(updated.cwOpportunityId);
return new OpportunityController(updated, {
company: updated.company
? await buildCompanyController(updated.company)
: undefined,
customFields: cwData.customFields ?? [],
activities,
});
},
/**
* Fetch All Opportunities (Paginated)
*
* @param page - Page number (1-based)
* @param rpp - Records per page
* @param opts - Optional filters
* @returns {Promise<OpportunityController[]>}
*/
async fetchPages(
page: number,
rpp: number,
opts?: { includeClosed?: boolean },
): Promise<OpportunityController[]> {
const skip = (Math.max(page, 1) - 1) * rpp;
const items = await prisma.opportunity.findMany({
where: opts?.includeClosed ? undefined : { closedFlag: false },
include: { company: true },
skip,
take: rpp,
orderBy: { createdAt: "desc" },
});
return Promise.all(
items.map(
async (item) =>
new OpportunityController(item, {
company: item.company
? await buildCompanyController(item.company)
: undefined,
activities: await buildActivities(item.cwOpportunityId),
}),
),
);
},
/**
* Search Opportunities
*
* Search opportunities by name, company name, contact name, notes,
* sales rep, or status with pagination support.
*
* @param query - Search query string
* @param page - Page number (1-based)
* @param rpp - Records per page
* @param opts - Optional filters
* @returns {Promise<OpportunityController[]>}
*/
async search(
query: string,
page: number,
rpp: number,
opts?: { includeClosed?: boolean },
): Promise<OpportunityController[]> {
const skip = (Math.max(page, 1) - 1) * rpp;
const numericQuery = /^\d+$/.test(query.trim())
? Number(query.trim())
: null;
const items = await prisma.opportunity.findMany({
where: {
...(opts?.includeClosed ? {} : { closedFlag: false }),
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ companyName: { contains: query, mode: "insensitive" } },
{ contactName: { contains: query, mode: "insensitive" } },
{ notes: { contains: query, mode: "insensitive" } },
{ primarySalesRepName: { contains: query, mode: "insensitive" } },
{ statusName: { contains: query, mode: "insensitive" } },
{ stageName: { contains: query, mode: "insensitive" } },
...(numericQuery !== null
? [{ cwOpportunityId: { equals: numericQuery } }]
: []),
],
},
include: { company: true },
skip,
take: rpp,
orderBy: { expectedCloseDate: "asc" },
});
return Promise.all(
items.map(
async (item) =>
new OpportunityController(item, {
company: item.company
? await buildCompanyController(item.company)
: undefined,
activities: await buildActivities(item.cwOpportunityId),
}),
),
);
},
/**
* Count Opportunities
*
* @param opts - Optional filters
* @returns {Promise<number>}
*/
async count(opts?: { openOnly?: boolean }): Promise<number> {
return prisma.opportunity.count({
where: opts?.openOnly ? { closedFlag: false } : undefined,
});
},
/**
* Count Search Results
*
* Returns the total number of opportunities matching a search query,
* using the same filter logic as `search()`.
*
* @param query - Search query string
* @param opts - Optional filters
* @returns {Promise<number>}
*/
async searchCount(
query: string,
opts?: { includeClosed?: boolean },
): Promise<number> {
const numericQuery = /^\d+$/.test(query.trim())
? Number(query.trim())
: null;
return prisma.opportunity.count({
where: {
...(opts?.includeClosed ? {} : { closedFlag: false }),
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ companyName: { contains: query, mode: "insensitive" } },
{ contactName: { contains: query, mode: "insensitive" } },
{ notes: { contains: query, mode: "insensitive" } },
{ primarySalesRepName: { contains: query, mode: "insensitive" } },
{ statusName: { contains: query, mode: "insensitive" } },
{ stageName: { contains: query, mode: "insensitive" } },
...(numericQuery !== null
? [{ cwOpportunityId: { equals: numericQuery } }]
: []),
],
},
});
},
/**
* Fetch Opportunities by Company
*
* Fetch all opportunities for a company by its internal company ID.
*
* @param companyId - The internal company ID
* @param opts - Optional filters
* @returns {Promise<OpportunityController[]>}
*/
async fetchByCompany(
companyId: string,
opts?: { includeClosed?: boolean },
): Promise<OpportunityController[]> {
const items = await prisma.opportunity.findMany({
where: {
companyId,
...(opts?.includeClosed ? {} : { closedFlag: false }),
},
include: { company: true },
orderBy: { expectedCloseDate: "asc" },
});
return Promise.all(
items.map(
async (item) =>
new OpportunityController(item, {
company: item.company
? await buildCompanyController(item.company)
: undefined,
activities: await buildActivities(item.cwOpportunityId),
}),
),
);
},
};
+332
View File
@@ -0,0 +1,332 @@
import { prisma } from "../constants";
import { CatalogItemController } from "../controllers/CatalogItemController";
import GenericError from "../Errors/GenericError";
import {
getSubcategoriesForCategory,
getSubcategoriesForGroup,
ECOSYSTEM_TREE,
} from "../modules/catalog-categories/catalogCategories";
/**
* Standard include clause used by catalog item queries.
* Includes one level of linked items.
*/
const catalogItemInclude = {
linkedItems: true,
} as const;
/**
* Filter options for catalog item queries.
*/
export interface CatalogFilterOpts {
includeInactive?: boolean;
category?: string;
subcategory?: string;
group?: string;
manufacturer?: string;
ecosystem?: string;
inStock?: boolean;
minPrice?: number;
maxPrice?: number;
}
/**
* Builds a Prisma `where` clause from filter options.
*/
function buildFilterWhere(opts: CatalogFilterOpts = {}) {
const conditions: Record<string, unknown>[] = [];
if (!opts.includeInactive) {
conditions.push({ inactive: false });
}
if (opts.category) {
conditions.push({ category: opts.category });
}
if (opts.subcategory) {
conditions.push({ subcategory: opts.subcategory });
}
if (opts.group && opts.category) {
const subcats = getSubcategoriesForGroup(opts.category, opts.group);
if (subcats.length > 0) {
conditions.push({ subcategory: { in: subcats } });
}
} else if (opts.group && !opts.category) {
// Try to find the group in any category
const {
CATEGORY_TREE,
isCategoryGroup,
} = require("../modules/catalog-categories/catalogCategories");
for (const cat of CATEGORY_TREE) {
const subcats = getSubcategoriesForGroup(cat.name, opts.group);
if (subcats.length > 0) {
conditions.push({ category: cat.name, subcategory: { in: subcats } });
break;
}
}
}
if (opts.manufacturer) {
conditions.push({
manufacturer: { contains: opts.manufacturer, mode: "insensitive" },
});
}
if (opts.ecosystem) {
const eco = ECOSYSTEM_TREE.find(
(e) => e.name.toLowerCase() === opts.ecosystem!.toLowerCase(),
);
if (eco && eco.manufacturers.length > 0) {
conditions.push({
OR: eco.manufacturers.map((m) => ({
manufacturer: { contains: m.name, mode: "insensitive" as const },
subcategory: { startsWith: m.subcategoryPrefix },
category: m.category,
})),
});
}
}
if (opts.inStock) {
conditions.push({ onHand: { gt: 0 } });
}
if (opts.minPrice !== undefined) {
conditions.push({ price: { gte: opts.minPrice } });
}
if (opts.maxPrice !== undefined) {
conditions.push({ price: { lte: opts.maxPrice } });
}
return conditions.length > 0 ? { AND: conditions } : undefined;
}
export const procurement = {
/**
* Fetch Catalog Item
*
* Fetch a catalog item by its internal ID or ConnectWise catalog ID
* and return a CatalogItemController instance.
*
* @param identifier - The internal ID (string) or CW catalog ID (number)
* @returns {Promise<CatalogItemController>} - The catalog item controller
*/
async fetchItem(identifier: string | number): Promise<CatalogItemController> {
const isNumeric =
typeof identifier === "number" || /^\d+$/.test(String(identifier));
const item = await prisma.catalogItem.findFirst({
where: isNumeric
? { cwCatalogId: Number(identifier) }
: {
OR: [
{ id: identifier as string },
{ identifier: identifier as string },
],
},
include: catalogItemInclude,
});
if (!item) {
throw new GenericError({
message: "Catalog item not found",
name: "CatalogItemNotFound",
cause: `No catalog item exists with identifier '${identifier}'`,
status: 404,
});
}
return new CatalogItemController(item);
},
/**
* Fetch All Catalog Items (Paginated)
*
* Fetch pages of catalog items for pagination with optional filtering.
*
* @param page - Page number (1-based)
* @param rpp - Records per page
* @param opts - Filter options
* @returns {Promise<CatalogItemController[]>} - Array of catalog item controllers
*/
async fetchPages(
page: number,
rpp: number,
opts?: CatalogFilterOpts,
): Promise<CatalogItemController[]> {
const skip = (Math.max(page, 1) - 1) * rpp;
const take = rpp;
const items = await prisma.catalogItem.findMany({
where: buildFilterWhere(opts),
skip,
take,
include: catalogItemInclude,
orderBy: { name: "asc" },
});
return items.map((item) => new CatalogItemController(item));
},
/**
* Search Catalog Items
*
* Search catalog items by name, description, part number, or vendor SKU
* with pagination support and optional category/subcategory/ecosystem filters.
*
* @param query - Search query string
* @param page - Page number (1-based)
* @param rpp - Records per page
* @param opts - Filter options
* @returns {Promise<CatalogItemController[]>} - Array of matching catalog item controllers
*/
async search(
query: string,
page: number,
rpp: number,
opts?: CatalogFilterOpts,
): Promise<CatalogItemController[]> {
const skip = (Math.max(page, 1) - 1) * rpp;
const take = rpp;
const filterWhere = buildFilterWhere(opts) ?? {};
const items = await prisma.catalogItem.findMany({
where: {
...filterWhere,
OR: [
{ identifier: { contains: query, mode: "insensitive" } },
{ name: { contains: query, mode: "insensitive" } },
{ description: { contains: query, mode: "insensitive" } },
{ partNumber: { contains: query, mode: "insensitive" } },
{ vendorSku: { contains: query, mode: "insensitive" } },
{ manufacturer: { contains: query, mode: "insensitive" } },
],
},
skip,
take,
include: catalogItemInclude,
orderBy: { name: "asc" },
});
return items.map((item) => new CatalogItemController(item));
},
/**
* Count Catalog Items
*
* Returns the total number of catalog items matching the given filters.
*
* @param opts - Filter options
* @returns {Promise<number>} - Total count
*/
async count(
opts?: CatalogFilterOpts & { activeOnly?: boolean },
): Promise<number> {
// Support legacy `activeOnly` flag by mapping it to `includeInactive`
const filterOpts: CatalogFilterOpts = {
...opts,
includeInactive:
opts?.includeInactive ?? (opts?.activeOnly ? false : true),
};
if (opts?.activeOnly) filterOpts.includeInactive = false;
return prisma.catalogItem.count({
where: buildFilterWhere(filterOpts),
});
},
/**
* Count Catalog Items (with search query)
*
* Returns the total number of catalog items matching a search query and filters.
*
* @param query - Search query string
* @param opts - Filter options
* @returns {Promise<number>} - Total count
*/
async countSearch(query: string, opts?: CatalogFilterOpts): Promise<number> {
const filterWhere = buildFilterWhere(opts) ?? {};
return prisma.catalogItem.count({
where: {
...filterWhere,
OR: [
{ identifier: { contains: query, mode: "insensitive" } },
{ name: { contains: query, mode: "insensitive" } },
{ description: { contains: query, mode: "insensitive" } },
{ partNumber: { contains: query, mode: "insensitive" } },
{ vendorSku: { contains: query, mode: "insensitive" } },
{ manufacturer: { contains: query, mode: "insensitive" } },
],
},
});
},
/**
* Fetch Distinct Values
*
* Returns the distinct values for a given field across all catalog items.
* Useful for populating filter dropdowns in the UI.
*
* @param field - The field to get distinct values for
* @param opts - Filter options to scope the distinct query
* @returns {Promise<string[]>} - Sorted array of distinct non-null values
*/
async fetchDistinctValues(
field: "category" | "subcategory" | "manufacturer",
opts?: CatalogFilterOpts,
): Promise<string[]> {
const items = await prisma.catalogItem.findMany({
where: buildFilterWhere(opts),
select: { [field]: true },
distinct: [field],
orderBy: { [field]: "asc" },
});
return items
.map((item: Record<string, unknown>) => item[field] as string | null)
.filter((v): v is string => v !== null);
},
/**
* Link Catalog Items
*
* Links a target catalog item to a source catalog item.
*
* @param sourceIdentifier - The source item's internal ID, identifier, or CW catalog ID
* @param targetIdentifier - The target item's internal ID, identifier, or CW catalog ID
* @returns {Promise<CatalogItemController>} - The updated source controller with linked items
*/
async linkItems(
sourceIdentifier: string | number,
targetIdentifier: string | number,
): Promise<CatalogItemController> {
const source = await procurement.fetchItem(sourceIdentifier);
const target = await procurement.fetchItem(targetIdentifier);
return source.linkItem(target.id);
},
/**
* Unlink Catalog Items
*
* Removes the link between a source catalog item and a target catalog item.
*
* @param sourceIdentifier - The source item's internal ID, identifier, or CW catalog ID
* @param targetIdentifier - The target item's internal ID, identifier, or CW catalog ID
* @returns {Promise<CatalogItemController>} - The updated source controller
*/
async unlinkItems(
sourceIdentifier: string | number,
targetIdentifier: string | number,
): Promise<CatalogItemController> {
const source = await procurement.fetchItem(sourceIdentifier);
const target = await procurement.fetchItem(targetIdentifier);
return source.unlinkItem(target.id);
},
};
+7
View File
@@ -4,6 +4,7 @@ import { prisma } from "../constants";
import { SessionTokensObject } from "../controllers/SessionController"; import { SessionTokensObject } from "../controllers/SessionController";
import UserController from "../controllers/UserController"; import UserController from "../controllers/UserController";
import { fetchMicrosoftUser } from "../modules/fetchMicrosoftUser"; import { fetchMicrosoftUser } from "../modules/fetchMicrosoftUser";
import { findCwIdentifierByEmail } from "../modules/cw-utils/members/fetchAllMembers";
import { events } from "../modules/globalEvents"; import { events } from "../modules/globalEvents";
import { sessions } from "./sessions"; import { sessions } from "./sessions";
import * as msal from "@azure/msal-node"; import * as msal from "@azure/msal-node";
@@ -90,12 +91,18 @@ export const users = {
async createUser(token: string): Promise<UserController> { async createUser(token: string): Promise<UserController> {
const msData = await fetchMicrosoftUser(token); const msData = await fetchMicrosoftUser(token);
// Attempt to resolve the user's ConnectWise identifier by email
const cwIdentifier = await findCwIdentifierByEmail(msData.mail).catch(
() => null,
);
const newUser = await prisma.user.create({ const newUser = await prisma.user.create({
data: { data: {
userId: msData.id, userId: msData.id,
email: msData.mail, email: msData.mail,
name: `${msData.givenName} ${msData.surname}`, name: `${msData.givenName} ${msData.surname}`,
login: msData.userPrincipalName, login: msData.userPrincipalName,
cwIdentifier,
token, token,
}, },
include: { roles: true }, include: { roles: true },
@@ -0,0 +1,498 @@
/**
* Catalog Categories & Ecosystems
*
* This module defines the complete category/subcategory hierarchy and
* ecosystem decision trees used for product filtering in the UI.
*
* --- Terminology ---
*
* Category: Top-level CW category (e.g. "Technology", "Field", "General").
* A category is NEVER a subcategory.
*
* Subcategory: The CW subcategory name stored on each catalog item.
* At the second level of the tree, if there are no children
* beneath it then the node name IS the subcategory.
* If children exist, the second-level node is an *umbrella*
* that groups related subcategories the children are the
* actual subcategory names.
*
* Ecosystem: A cross-cutting product grouping defined by manufacturer +
* category + subcategory-prefix rules. Ecosystems let the UI
* present a "Networking" or "Video Surveillance" view that
* spans manufacturers regardless of where CW filed them.
*
* --- Data shapes ---
*
* SubcategoryNode a leaf: `{ name, cwId? }`
* CategoryGroup an umbrella with children: `{ name, children[] }`
* CategoryEntry either a leaf OR a group at the 2nd level
* TopLevelCategory `{ name, cwId?, entries[] }`
*
* The `CATEGORY_TREE` export is the single source of truth; helpers derive
* flat lists, lookup maps, and search predicates from it.
*/
// ─── Data types ──────────────────────────────────────────────────────────────
export interface SubcategoryNode {
/** The exact CW subcategory name */
name: string;
/** CW subcategory id (optional, for reference) */
cwId?: number;
}
export interface CategoryGroup {
/** Display name of the umbrella (e.g. "Network", "Cables", "AlarmBurg") */
name: string;
/** The subcategories that belong to this umbrella */
children: SubcategoryNode[];
}
/** A second-level entry is either a direct subcategory or an umbrella group */
export type CategoryEntry = SubcategoryNode | CategoryGroup;
export interface TopLevelCategory {
/** The CW category name */
name: string;
/** CW category id (optional, for reference) */
cwId?: number;
/** Second-level entries under this category */
entries: CategoryEntry[];
}
/** Helper type guard */
export function isCategoryGroup(entry: CategoryEntry): entry is CategoryGroup {
return "children" in entry;
}
// ─── Ecosystem types ─────────────────────────────────────────────────────────
export interface EcosystemManufacturer {
/** Manufacturer name as stored in CW */
name: string;
/** CW manufacturer id */
cwId?: number;
/** Which CW category these products fall under */
category: string;
/** Subcategory prefix — matches any subcategory starting with this string */
subcategoryPrefix: string;
}
export interface Ecosystem {
/** Display name (e.g. "Networking", "Video Surveillance") */
name: string;
/** Manufacturers that belong to this ecosystem */
manufacturers: EcosystemManufacturer[];
}
// ─── Category Tree ───────────────────────────────────────────────────────────
export const CATEGORY_TREE: TopLevelCategory[] = [
{
name: "Technology",
cwId: 18,
entries: [
{ name: "GeneralEquip", cwId: 57 },
{ name: "Home Entertainment", cwId: 114 },
{ name: "Monitor", cwId: 115 },
{ name: "Printers", cwId: 120 },
{ name: "Storage", cwId: 108 },
{
name: "Network",
children: [
{ name: "Network-Other", cwId: 174 },
{ name: "Network-Router", cwId: 119 },
{ name: "Network-Switch", cwId: 112 },
{ name: "Network-Wireless", cwId: 111 },
],
},
{
name: "Computer",
children: [
{ name: "Computer-Components", cwId: 109 },
{ name: "Computer-Desktop", cwId: 106 },
{ name: "Computer-Laptop", cwId: 107 },
],
},
{
name: "Recurring",
children: [
{ name: "Recurring - Online", cwId: 83 },
{ name: "Recurring - Other", cwId: 84 },
{ name: "Recurring - Protection", cwId: 81 },
{ name: "Recurring - Telephone", cwId: 133 },
],
},
{
name: "Telephone",
children: [
{ name: "Tele-HSet-Digital", cwId: 116 },
{ name: "Tele-HSet-IP", cwId: 206 },
{ name: "Tele-HSet-SLT" },
{ name: "Tele-Misc", cwId: 75 },
{ name: "Tele-Paging", cwId: 76 },
{ name: "Tele-SystemCards", cwId: 135 },
{ name: "Tele-Systems", cwId: 78 },
],
},
],
},
{
name: "General",
cwId: 25,
entries: [
{ name: "Batteries", cwId: 80 },
{ name: "Battery Backups", cwId: 144 },
{ name: "BulkWire", cwId: 200 },
{
name: "Cables",
children: [
{ name: "Cables-Adapters", cwId: 182 },
{ name: "Cables-HDMI", cwId: 176 },
{ name: "Cables-Network", cwId: 87 },
{ name: "Cables-Other", cwId: 177 },
{ name: "Cables-USB", cwId: 178 },
{ name: "Cables-VGA", cwId: 179 },
],
},
{ name: "Elec Cords & Adapters", cwId: 142 },
{ name: "Enclosures", cwId: 141 },
{ name: "PowerSupply", cwId: 167 },
{
name: "RackEquip",
children: [
{ name: "RackEquip-Rack", cwId: 143 },
{ name: "RackEquip-Shelves", cwId: 190 },
],
},
],
},
{
name: "Field",
cwId: 28,
entries: [
{ name: "Conduit" },
{ name: "Electric", cwId: 199 },
{ name: "GateControl", cwId: 45 },
{ name: "Locksets" },
{ name: "Other", cwId: 46 },
{ name: "Relays", cwId: 168 },
{
name: "AccessControl",
children: [
{ name: "AccessControl-Controllers", cwId: 137 },
{ name: "AccessControl-Credential", cwId: 183 },
{ name: "AccessControl-LockDevices", cwId: 138 },
{ name: "AccessControl-Other", cwId: 44 },
{ name: "AccessControl-Readers", cwId: 136 },
{ name: "AccessControl-VideoEntry", cwId: 139 },
],
},
{
name: "AlarmBurg",
children: [
{ name: "AlarmBurg-Communicators", cwId: 96 },
{ name: "AlarmBurg-Keypads", cwId: 93 },
{ name: "AlarmBurg-Modules", cwId: 140 },
{ name: "AlarmBurg-Other", cwId: 92 },
{ name: "AlarmBurg-Panels", cwId: 42 },
{ name: "AlarmBurg-Sensors-Wireless", cwId: 147 },
{ name: "AlarmBurg-Sensors-Wired", cwId: 146 },
{ name: "AlarmBurg-Siren", cwId: 145 },
],
},
{
name: "AlarmFire",
children: [
{ name: "AlarmFire-Communicators", cwId: 97 },
{ name: "AlarmFire-Devices", cwId: 169 },
{ name: "AlarmFire-Modules", cwId: 170 },
{ name: "AlarmFire-Other", cwId: 98 },
{ name: "AlarmFire-Panels", cwId: 95 },
{ name: "AlarmFire-Sensors", cwId: 94 },
],
},
{
name: "Automation",
children: [
{ name: "Automation-General", cwId: 99 },
{ name: "Automation-HVAC", cwId: 181 },
{ name: "Automation-Lights", cwId: 180 },
{ name: "Automation-Locks", cwId: 192 },
{ name: "Automation-Thermostat" },
],
},
{
name: "AV",
children: [
{ name: "AV-Adapters&Cables", cwId: 171 },
{ name: "AV-Components", cwId: 172 },
{ name: "AV-Mounts", cwId: 191 },
{ name: "AV-Other", cwId: 184 },
{ name: "AV-Speakers", cwId: 173 },
{ name: "AV-Television", cwId: 175 },
],
},
{
name: "StrCbl",
children: [
{ name: "StrCbl-Jacks", cwId: 186 },
{ name: "StrCbl-PatchPanel", cwId: 187 },
{ name: "StrCbl-Plates", cwId: 185 },
],
},
{
name: "Surveillance",
children: [
{ name: "Surveillance-Accs", cwId: 90 },
{ name: "Surveillance-CamerasAnalog", cwId: 89 },
{ name: "Surveillance-CamerasIP", cwId: 88 },
{ name: "Surveillance-NVR", cwId: 43 },
],
},
],
},
];
// ─── Ecosystem Tree ──────────────────────────────────────────────────────────
export const ECOSYSTEM_TREE: Ecosystem[] = [
{
name: "Networking",
manufacturers: [
{
name: "Ubiquiti",
cwId: 248,
category: "Technology",
subcategoryPrefix: "Network-",
},
{
name: "TP-Link",
cwId: 259,
category: "Technology",
subcategoryPrefix: "Network-",
},
],
},
{
name: "Video Surveillance",
manufacturers: [
{
name: "Uniview",
cwId: 239,
category: "Field",
subcategoryPrefix: "Surveillance-",
},
{
name: "Hikvision",
cwId: 299,
category: "Field",
subcategoryPrefix: "Surveillance-",
},
{
name: "Alarm.com",
cwId: 294,
category: "Field",
subcategoryPrefix: "Surveillance-",
},
],
},
{
name: "Burg/Alarm",
manufacturers: [
{
name: "Qolsys",
cwId: 376,
category: "Field",
subcategoryPrefix: "AlarmBurg-",
},
{
name: "DSC",
cwId: 287,
category: "Field",
subcategoryPrefix: "AlarmBurg-",
},
],
},
];
// ─── Derived helpers ─────────────────────────────────────────────────────────
/**
* Returns a flat list of all subcategory names under a given category.
*/
export function getSubcategoriesForCategory(categoryName: string): string[] {
const category = CATEGORY_TREE.find((c) => c.name === categoryName);
if (!category) return [];
const subcats: string[] = [];
for (const entry of category.entries) {
if (isCategoryGroup(entry)) {
for (const child of entry.children) {
subcats.push(child.name);
}
} else {
subcats.push(entry.name);
}
}
return subcats;
}
/**
* Returns all subcategory names under a given umbrella group within a category.
* e.g. getSubcategoriesForGroup("Field", "AlarmBurg") ["AlarmBurg-Communicators", ...]
*/
export function getSubcategoriesForGroup(
categoryName: string,
groupName: string,
): string[] {
const category = CATEGORY_TREE.find((c) => c.name === categoryName);
if (!category) return [];
const group = category.entries.find(
(e) => isCategoryGroup(e) && e.name === groupName,
);
if (!group || !isCategoryGroup(group)) return [];
return group.children.map((c) => c.name);
}
/**
* Returns all top-level category names.
*/
export function getCategoryNames(): string[] {
return CATEGORY_TREE.map((c) => c.name);
}
/**
* Returns the umbrella group name for a given subcategory, or null if it's a
* direct entry (not under an umbrella).
*/
export function getGroupForSubcategory(
subcategoryName: string,
): { category: string; group: string } | null {
for (const cat of CATEGORY_TREE) {
for (const entry of cat.entries) {
if (isCategoryGroup(entry)) {
if (entry.children.some((c) => c.name === subcategoryName)) {
return { category: cat.name, group: entry.name };
}
}
}
}
return null;
}
/**
* Returns the full tree serialized for the API / UI consumption.
* Each top-level category includes its entries, with umbrella groups
* expanded to show children.
*/
export function serializeCategoryTree() {
return CATEGORY_TREE.map((cat) => ({
name: cat.name,
cwId: cat.cwId ?? null,
entries: cat.entries.map((entry) => {
if (isCategoryGroup(entry)) {
return {
type: "group" as const,
name: entry.name,
subcategories: entry.children.map((c) => ({
name: c.name,
cwId: c.cwId ?? null,
})),
};
}
return {
type: "subcategory" as const,
name: entry.name,
cwId: (entry as SubcategoryNode).cwId ?? null,
};
}),
}));
}
/**
* Returns the ecosystem tree serialized for the API / UI consumption.
*/
export function serializeEcosystemTree() {
return ECOSYSTEM_TREE.map((eco) => ({
name: eco.name,
manufacturers: eco.manufacturers.map((m) => ({
name: m.name,
cwId: m.cwId ?? null,
category: m.category,
subcategoryPrefix: m.subcategoryPrefix,
})),
}));
}
/**
* Returns a flat list of every known subcategory name across all categories.
*/
export function getAllSubcategoryNames(): string[] {
const names: string[] = [];
for (const cat of CATEGORY_TREE) {
for (const entry of cat.entries) {
if (isCategoryGroup(entry)) {
for (const child of entry.children) {
names.push(child.name);
}
} else {
names.push(entry.name);
}
}
}
return names;
}
/**
* Given a CW subcategory name, resolves which top-level category it belongs to.
*/
export function getCategoryForSubcategory(
subcategoryName: string,
): string | null {
for (const cat of CATEGORY_TREE) {
for (const entry of cat.entries) {
if (isCategoryGroup(entry)) {
if (entry.children.some((c) => c.name === subcategoryName)) {
return cat.name;
}
} else if (entry.name === subcategoryName) {
return cat.name;
}
}
}
return null;
}
/**
* Given a CW manufacturer name, returns which ecosystems it belongs to.
*/
export function getEcosystemsForManufacturer(
manufacturerName: string,
): string[] {
return ECOSYSTEM_TREE.filter((eco) =>
eco.manufacturers.some(
(m) => m.name.toLowerCase() === manufacturerName.toLowerCase(),
),
).map((eco) => eco.name);
}
/**
* Checks if a catalog item (by manufacturer + subcategory) matches a given ecosystem.
*/
export function matchesEcosystem(
ecosystemName: string,
manufacturer: string | null,
subcategory: string | null,
): boolean {
const eco = ECOSYSTEM_TREE.find((e) => e.name === ecosystemName);
if (!eco) return false;
return eco.manufacturers.some(
(m) =>
m.name.toLowerCase() === (manufacturer ?? "").toLowerCase() &&
(subcategory ?? "").startsWith(m.subcategoryPrefix),
);
}
@@ -0,0 +1,168 @@
import { Collection } from "@discordjs/collection";
import { connectWiseApi } from "../../../constants";
import {
CWActivity,
CWActivitySummary,
CWCreateActivity,
CWPatchOperation,
} from "./activity.types";
export const activityCw = {
/**
* Count Activities
*
* Returns the total number of activities in ConnectWise.
* Optionally accepts CW conditions string for filtered counts.
*/
countItems: async (conditions?: string): Promise<number> => {
const query = conditions
? `/sales/activities/count?conditions=${encodeURIComponent(conditions)}`
: "/sales/activities/count";
const response = await connectWiseApi.get(query);
return response.data.count;
},
/**
* Fetch All Activity Summaries
*
* Lightweight fetch returning only id and _info (for lastUpdated comparison).
* Paginates through all activities.
*/
fetchAllSummaries: async (): Promise<
Collection<number, CWActivitySummary>
> => {
const allItems = new Collection<number, CWActivitySummary>();
const pageSize = 1000;
const count = await activityCw.countItems();
const totalPages = Math.ceil(count / pageSize);
for (let page = 0; page < totalPages; page++) {
const response = await connectWiseApi.get(
`/sales/activities?page=${page + 1}&pageSize=${pageSize}&fields=id,_info`,
);
const items: CWActivitySummary[] = response.data;
for (const item of items) {
allItems.set(item.id, item);
}
}
return allItems;
},
/**
* Fetch All Activities (Full)
*
* Fetches all activities with complete data. Paginates through
* the full list. Optionally accepts CW conditions string for filtering.
*/
fetchAll: async (
conditions?: string,
): Promise<Collection<number, CWActivity>> => {
const allItems = new Collection<number, CWActivity>();
const pageSize = 1000;
const count = await activityCw.countItems(conditions);
const totalPages = Math.ceil(count / pageSize);
for (let page = 0; page < totalPages; page++) {
const conditionsParam = conditions
? `&conditions=${encodeURIComponent(conditions)}`
: "";
const response = await connectWiseApi.get(
`/sales/activities?page=${page + 1}&pageSize=${pageSize}${conditionsParam}`,
);
const items: CWActivity[] = response.data;
for (const item of items) {
allItems.set(item.id, item);
}
}
return allItems;
},
/**
* Fetch Single Activity
*
* Fetches a single activity by its ConnectWise ID.
*/
fetch: async (id: number): Promise<CWActivity> => {
const response = await connectWiseApi.get(`/sales/activities/${id}`);
return response.data;
},
/**
* Fetch Activities by Company
*
* Fetches all activities associated with a specific ConnectWise company ID.
*/
fetchByCompany: async (
cwCompanyId: number,
): Promise<Collection<number, CWActivity>> => {
return activityCw.fetchAll(`company/id=${cwCompanyId}`);
},
/**
* Fetch Activities by Opportunity
*
* Fetches all activities associated with a specific opportunity ID.
*/
fetchByOpportunity: async (
opportunityId: number,
): Promise<Collection<number, CWActivity>> => {
return activityCw.fetchAll(`opportunity/id=${opportunityId}`);
},
/**
* Create Activity
*
* Creates a new activity in ConnectWise.
*/
create: async (activity: CWCreateActivity): Promise<CWActivity> => {
const response = await connectWiseApi.post("/sales/activities", activity);
return response.data;
},
/**
* Update Activity (PATCH)
*
* Updates an existing activity using JSON Patch operations.
*/
update: async (
id: number,
operations: CWPatchOperation[],
): Promise<CWActivity> => {
const response = await connectWiseApi.patch(
`/sales/activities/${id}`,
operations,
);
return response.data;
},
/**
* Replace Activity (PUT)
*
* Replaces an entire activity record in ConnectWise.
*/
replace: async (
id: number,
activity: CWCreateActivity,
): Promise<CWActivity> => {
const response = await connectWiseApi.put(
`/sales/activities/${id}`,
activity,
);
return response.data;
},
/**
* Delete Activity
*
* Deletes an activity by its ConnectWise ID.
*/
delete: async (id: number): Promise<void> => {
await connectWiseApi.delete(`/sales/activities/${id}`);
},
};
@@ -0,0 +1,123 @@
interface CWReference {
id: number;
name: string;
_info?: Record<string, string>;
}
interface CWMemberReference {
id: number;
identifier: string;
name: string;
_info?: Record<string, string>;
}
interface CWCompanyReference {
id: number;
identifier: string;
name: string;
_info?: Record<string, string>;
}
interface CWContactReference {
id: number;
name: string;
_info?: Record<string, string>;
}
export interface CWActivity {
id: number;
name: string;
type: CWReference;
company: CWCompanyReference;
contact: CWContactReference;
phoneNumber: string;
email: string;
status: CWReference;
opportunity: CWReference;
ticket: CWReference;
agreement: CWReference;
campaign: CWReference;
notes: string;
dateStart: string;
dateEnd: string;
assignTo: CWMemberReference;
scheduleStatus: CWReference;
reminder: CWReference;
where: CWReference;
notifyFlag: boolean;
mobileGuid: string;
currency: CWReference;
customFields: CWActivityCustomField[];
_info: CWActivityInfo;
}
export interface CWActivityCustomField {
id: number;
caption: string;
type: string;
entryMethod: string;
numberOfDecimals: number;
value: unknown;
}
export interface CWActivityInfo {
lastUpdated: string;
updatedBy: string;
dateEntered: string;
enteredBy: string;
}
export interface CWActivitySummary {
id: number;
_info?: Record<string, string>;
}
export interface CWCreateActivity {
name: string;
type?: { id: number };
company?: { id: number };
contact?: { id: number };
phoneNumber?: string;
email?: string;
status?: { id: number };
opportunity?: { id: number };
ticket?: { id: number };
agreement?: { id: number };
campaign?: { id: number };
notes?: string;
dateStart?: string;
dateEnd?: string;
assignTo?: { id: number };
scheduleStatus?: { id: number };
reminder?: { id: number };
where?: { id: number };
notifyFlag?: boolean;
}
export interface CWUpdateActivity {
name?: string;
type?: { id: number };
company?: { id: number };
contact?: { id: number };
phoneNumber?: string;
email?: string;
status?: { id: number };
opportunity?: { id: number };
ticket?: { id: number };
agreement?: { id: number };
campaign?: { id: number };
notes?: string;
dateStart?: string;
dateEnd?: string;
assignTo?: { id: number };
scheduleStatus?: { id: number };
reminder?: { id: number };
where?: { id: number };
notifyFlag?: boolean;
}
export interface CWPatchOperation {
op: "replace" | "add" | "remove";
path: string;
value: unknown;
}
@@ -0,0 +1,27 @@
import GenericError from "../../../Errors/GenericError";
import { activityCw } from "./activities";
import { CWActivity, CWCreateActivity } from "./activity.types";
/**
* Create a new activity in ConnectWise.
*
* @param activity - The activity data to create
* @returns The newly created CW activity object
* @throws GenericError if the creation fails
*/
export const createActivity = async (
activity: CWCreateActivity,
): Promise<CWActivity> => {
try {
return await activityCw.create(activity);
} catch (error) {
const errBody = (error as any).response?.data || error;
console.error("Error creating activity:", errBody);
throw new GenericError({
name: "CreateActivityError",
message: "Failed to create activity in ConnectWise",
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
};
@@ -0,0 +1,27 @@
import GenericError from "../../../Errors/GenericError";
import { activityCw } from "./activities";
import { CWActivity } from "./activity.types";
/**
* Fetch a single activity by its ConnectWise ID.
*
* @param cwActivityId - The ConnectWise activity ID
* @returns The full CW activity object
* @throws GenericError if the fetch fails
*/
export const fetchActivity = async (
cwActivityId: number,
): Promise<CWActivity> => {
try {
return await activityCw.fetch(cwActivityId);
} catch (error) {
const errBody = (error as any).response?.data || error;
console.error(`Error fetching activity with ID ${cwActivityId}:`, errBody);
throw new GenericError({
name: "FetchActivityError",
message: `Failed to fetch activity ${cwActivityId}`,
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
};
@@ -0,0 +1,28 @@
import { Collection } from "@discordjs/collection";
import GenericError from "../../../Errors/GenericError";
import { activityCw } from "./activities";
import { CWActivity } from "./activity.types";
/**
* Fetch all activities from ConnectWise with optional conditions.
*
* @param conditions - Optional CW conditions string for filtering
* @returns A Collection of CW activities keyed by their ID
* @throws GenericError if the fetch fails
*/
export const fetchAllActivities = async (
conditions?: string,
): Promise<Collection<number, CWActivity>> => {
try {
return await activityCw.fetchAll(conditions);
} catch (error) {
const errBody = (error as any).response?.data || error;
console.error("Error fetching all activities:", errBody);
throw new GenericError({
name: "FetchAllActivitiesError",
message: "Failed to fetch activities from ConnectWise",
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
};
+15
View File
@@ -0,0 +1,15 @@
export { activityCw } from "./activities";
export { fetchActivity } from "./fetchActivity";
export { fetchAllActivities } from "./fetchAllActivities";
export { createActivity } from "./createActivity";
export { updateActivity } from "./updateActivity";
export type {
CWActivity,
CWActivitySummary,
CWActivityCustomField,
CWActivityInfo,
CWCreateActivity,
CWUpdateActivity,
CWPatchOperation,
} from "./activity.types";
@@ -0,0 +1,29 @@
import GenericError from "../../../Errors/GenericError";
import { activityCw } from "./activities";
import { CWActivity, CWPatchOperation } from "./activity.types";
/**
* Update an existing activity in ConnectWise using JSON Patch operations.
*
* @param cwActivityId - The ConnectWise activity ID to update
* @param operations - Array of JSON Patch operations to apply
* @returns The updated CW activity object
* @throws GenericError if the update fails
*/
export const updateActivity = async (
cwActivityId: number,
operations: CWPatchOperation[],
): Promise<CWActivity> => {
try {
return await activityCw.update(cwActivityId, operations);
} catch (error) {
const errBody = (error as any).response?.data || error;
console.error(`Error updating activity with ID ${cwActivityId}:`, errBody);
throw new GenericError({
name: "UpdateActivityError",
message: `Failed to update activity ${cwActivityId}`,
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
};
@@ -0,0 +1,67 @@
import { Collection } from "@discordjs/collection";
import { connectWiseApi } from "../../../constants";
export interface CWMember {
id: number;
identifier: string;
firstName: string;
lastName: string;
officeEmail: string;
inactiveFlag: boolean;
_info: Record<string, string>;
}
/**
* Fetch All CW Members
*
* Fetches every member from ConnectWise using pagination and returns them
* in a Collection keyed by their identifier (e.g. "jroberts").
*
* @returns {Promise<Collection<string, CWMember>>} Collection of CW members keyed by identifier
*/
export const fetchAllCwMembers = async (): Promise<
Collection<string, CWMember>
> => {
const members = new Collection<string, CWMember>();
const pageSize = 1000;
const { data: countData } = await connectWiseApi.get("/system/members/count");
const totalPages = Math.ceil(countData.count / pageSize);
for (let page = 0; page < totalPages; page++) {
const { data } = await connectWiseApi.get<CWMember[]>(
`/system/members?page=${page + 1}&pageSize=${pageSize}`,
);
for (const member of data) {
members.set(member.identifier, member);
}
}
return members;
};
/**
* Find CW Member Identifier by Email
*
* Looks up a ConnectWise member whose `officeEmail` matches the provided
* email address (case-insensitive) and returns their `identifier` string
* (e.g. "jroberts"). Returns `null` if no match is found.
*
* @param email - The email address to search for
* @param members - Optional pre-fetched member collection to search against (avoids extra API call)
* @returns {Promise<string | null>} The CW identifier or null
*/
export const findCwIdentifierByEmail = async (
email: string,
members?: Collection<string, CWMember>,
): Promise<string | null> => {
const allMembers = members ?? (await fetchAllCwMembers());
const normalised = email.toLowerCase();
const match = allMembers.find(
(m) => m.officeEmail?.toLowerCase() === normalised,
);
return match?.identifier ?? null;
};
+104
View File
@@ -0,0 +1,104 @@
import { Collection } from "@discordjs/collection";
import { prisma } from "../../../constants";
import { redis } from "../../../constants";
import { CWMember } from "./fetchAllMembers";
const REDIS_KEY = "cw:members";
export interface ResolvedMember {
/** Local database user ID (null if no matching local user) */
id: string | null;
/** CW member identifier (e.g. "jroberts") */
identifier: string;
/** Full name resolved from CW member cache, or raw identifier as fallback */
name: string;
/** ConnectWise member ID */
cwMemberId: number | null;
}
/**
* CW Member Cache
*
* Dual-layer cache (in-memory + Redis) of ConnectWise members keyed by
* their identifier (e.g. "jroberts"). Populated by `refreshCwIdentifiers`
* on startup and every 30 minutes thereafter.
*/
let memberCache = new Collection<string, CWMember>();
/**
* Set the member cache contents.
*
* Replaces both the in-memory Collection and the Redis snapshot.
*
* @param members - Collection of CW members keyed by identifier
*/
export const setMemberCache = async (members: Collection<string, CWMember>) => {
memberCache = members;
await redis.set(REDIS_KEY, JSON.stringify([...members.values()]));
};
/**
* Get the current member cache.
*
* Returns the in-memory Collection. If empty, attempts to hydrate from Redis
* first. Returns whatever is available (may be empty if Redis is also cold).
*/
export const getMemberCache = async (): Promise<
Collection<string, CWMember>
> => {
if (memberCache.size > 0) return memberCache;
const stored = await redis.get(REDIS_KEY);
if (stored) {
const parsed: CWMember[] = JSON.parse(stored);
memberCache = new Collection(parsed.map((m) => [m.identifier, m]));
}
return memberCache;
};
/**
* Resolve CW Identifier to Full Name
*
* Looks up a ConnectWise member by their identifier in the in-memory cache
* and returns their full name. Falls back to the raw identifier if not found.
*
* @param identifier - The CW member identifier (e.g. "jroberts")
* @returns The member's full name (e.g. "John Roberts") or the raw identifier
*/
export const resolveMemberName = (identifier: string): string => {
const member = memberCache.get(identifier);
if (!member) return identifier;
return `${member.firstName} ${member.lastName}`.trim() || identifier;
};
/**
* Resolve CW Identifier to Full Member Info
*
* Looks up a ConnectWise member by their identifier in the in-memory cache
* and cross-references with the local database to return a complete member
* reference including local user ID, CW identifier, full name, and CW member ID.
*
* @param identifier - The CW member identifier (e.g. "jroberts")
* @returns {Promise<ResolvedMember>} Resolved member info
*/
export const resolveMember = async (
identifier: string,
): Promise<ResolvedMember> => {
const cwMember = memberCache.get(identifier);
const name = cwMember
? `${cwMember.firstName} ${cwMember.lastName}`.trim() || identifier
: identifier;
const localUser = await prisma.user.findFirst({
where: { cwIdentifier: identifier },
select: { id: true },
});
return {
id: localUser?.id ?? null,
identifier,
name,
cwMemberId: cwMember?.id ?? null,
};
};
@@ -0,0 +1,46 @@
import { connectWiseApi, prisma } from "../../../constants";
import { events } from "../../globalEvents";
import { fetchAllCwMembers, findCwIdentifierByEmail } from "./fetchAllMembers";
import { setMemberCache } from "./memberCache";
/**
* Refresh CW Identifiers
*
* Fetches all CW members and all users from the database, then updates
* each user's `cwIdentifier` field by matching their email to a CW member's
* `officeEmail`. Only users whose identifier has changed (or was previously
* null) are updated to avoid unnecessary writes.
*
* Also refreshes the in-memory member cache used for name resolution.
*/
export const refreshCwIdentifiers = async () => {
events.emit("cw:members:refresh:started");
const allMembers = await fetchAllCwMembers();
await setMemberCache(allMembers);
const allUsers = await prisma.user.findMany({
select: { id: true, email: true, cwIdentifier: true },
});
let updatedCount = 0;
await Promise.all(
allUsers.map(async (user) => {
const identifier = await findCwIdentifierByEmail(user.email, allMembers);
if (identifier !== user.cwIdentifier) {
await prisma.user.update({
where: { id: user.id },
data: { cwIdentifier: identifier },
});
updatedCount++;
}
}),
);
events.emit("cw:members:refresh:completed", {
totalMembers: allMembers.size,
totalUsers: allUsers.length,
usersUpdated: updatedCount,
});
};
@@ -0,0 +1,28 @@
import { Collection } from "@discordjs/collection";
import GenericError from "../../../Errors/GenericError";
import { opportunityCw } from "./opportunities";
import { CWOpportunity } from "./opportunity.types";
/**
* Fetch all opportunities from ConnectWise with optional conditions.
*
* @param conditions - Optional CW conditions string for filtering
* @returns A Collection of CW opportunities keyed by their ID
* @throws GenericError if the fetch fails
*/
export const fetchAllOpportunities = async (
conditions?: string,
): Promise<Collection<number, CWOpportunity>> => {
try {
return await opportunityCw.fetchAll(conditions);
} catch (error) {
const errBody = (error as any).response?.data || error;
console.error("Error fetching all opportunities:", errBody);
throw new GenericError({
name: "FetchAllOpportunitiesError",
message: "Failed to fetch opportunities from ConnectWise",
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
};
@@ -0,0 +1,31 @@
import { Collection } from "@discordjs/collection";
import GenericError from "../../../Errors/GenericError";
import { opportunityCw } from "./opportunities";
import { CWOpportunity } from "./opportunity.types";
/**
* Fetch all opportunities for a specific company from ConnectWise.
*
* @param cwCompanyId - The ConnectWise company ID
* @returns A Collection of CW opportunities for the company keyed by their ID
* @throws GenericError if the fetch fails
*/
export const fetchCompanyOpportunities = async (
cwCompanyId: number,
): Promise<Collection<number, CWOpportunity>> => {
try {
return await opportunityCw.fetchByCompany(cwCompanyId);
} catch (error) {
const errBody = (error as any).response?.data || error;
console.error(
`Error fetching opportunities for company ${cwCompanyId}:`,
errBody,
);
throw new GenericError({
name: "FetchCompanyOpportunitiesError",
message: `Failed to fetch opportunities for company ${cwCompanyId}`,
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
status: 502,
});
}
};

Some files were not shown because too many files have changed in this diff Show More