all the haul

This commit is contained in:
2026-04-07 23:56:31 +00:00
parent 87cce83030
commit 24f303355b
244 changed files with 33743 additions and 11249 deletions
-21
View File
@@ -5,7 +5,6 @@ import {
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
@@ -127,26 +126,6 @@ export class ActivityController {
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
*
+46 -21
View File
@@ -1,9 +1,27 @@
import { CatalogItem } from "../../generated/prisma/client";
import {
CatalogItem,
CatalogCategory,
CatalogSubcategory,
CatalogManufacturer,
} 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";
/**
* Shape of the Prisma result when catalog items are queried with
* `include: { linkedItems, manufacturer, subcategory: { include: { category } } }`.
*/
type CatalogItemWithRelations = CatalogItem & {
linkedItems?: (CatalogItem & {
manufacturer?: CatalogManufacturer | null;
subcategory?: CatalogSubcategory & { category?: CatalogCategory | null };
})[];
manufacturer?: CatalogManufacturer | null;
subcategory?: (CatalogSubcategory & { category?: CatalogCategory | null }) | null;
};
/**
* Catalog Item Controller
*
@@ -12,13 +30,15 @@ import GenericError from "../Errors/GenericError";
* the internal database representation with ConnectWise catalog data.
*/
export class CatalogItemController {
/** The ConnectWise catalog record ID (`CatalogItem.id` in Prisma — Int @unique). */
public readonly cwCatalogId: number;
/** The Prisma primary key (UUID). */
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;
@@ -48,24 +68,29 @@ export class CatalogItemController {
public readonly createdAt: Date;
public updatedAt: Date;
constructor(
itemData: CatalogItem & {
linkedItems?: CatalogItem[];
},
) {
this.id = itemData.id;
constructor(itemData: CatalogItemWithRelations) {
// `id` (Int @unique) is the ConnectWise catalog record ID.
// `uid` (String @id) is the Prisma primary key.
this.cwCatalogId = itemData.id;
this.id = itemData.uid;
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;
// Extract relation data into flat fields
const sub = itemData.subcategory;
const cat = sub?.category;
const mfr = itemData.manufacturer;
this.category = cat?.name ?? null;
this.categoryCwId = cat?.id ?? null;
this.subcategory = sub?.name ?? null;
this.subcategoryCwId = sub?.id ?? null;
this.manufacturer = mfr?.name ?? null;
this.manufactureCwId = mfr?.id ?? null;
this.partNumber = itemData.partNumber;
this.vendorName = itemData.vendorName;
this.vendorSku = itemData.vendorSku;
@@ -97,7 +122,7 @@ export class CatalogItemController {
if (onHand !== this.onHand) {
await prisma.catalogItem.update({
where: { id: this.id },
where: { uid: this.id },
data: { onHand },
});
this.onHand = onHand;
@@ -137,7 +162,7 @@ export class CatalogItemController {
}
const target = await prisma.catalogItem.findFirst({
where: { id: targetId },
where: { uid: targetId },
});
if (!target) {
@@ -150,9 +175,9 @@ export class CatalogItemController {
}
const updated = await prisma.catalogItem.update({
where: { id: this.id },
where: { uid: this.id },
data: {
linkedItems: { connect: { id: targetId } },
linkedItems: { connect: { uid: targetId } },
},
include: { linkedItems: true },
});
@@ -174,9 +199,9 @@ export class CatalogItemController {
*/
public async unlinkItem(targetId: string): Promise<CatalogItemController> {
const updated = await prisma.catalogItem.update({
where: { id: this.id },
where: { uid: this.id },
data: {
linkedItems: { disconnect: { id: targetId } },
linkedItems: { disconnect: { uid: targetId } },
},
include: { linkedItems: true },
});
+214 -148
View File
@@ -1,204 +1,270 @@
import { Company } from "../../generated/prisma/client";
import { connectWiseApi } from "../constants";
import { fetchCwCompanyById } from "../modules/cw-utils/fetchCompany";
import { fetchCompanyConfigurations } from "../modules/cw-utils/configurations/fetchCompanyConfigurations";
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";
Company,
CompanyAddress,
Contact,
} from "../../generated/prisma/client";
import { connectWiseApi, prisma } from "../constants";
import GenericError from "../Errors/GenericError";
import { processConfigurationResponse } from "../modules/cw-utils/configurations/processConfigurationResponse";
import { withCwRetry } from "../modules/cw-utils/withCwRetry";
import type { ConfigurationResponse } from "../types/ConnectWiseTypes";
// Type for company data with relations
type CompanyWithRelations = Company & {
contacts?: Contact[];
companyAddresses?: CompanyAddress[];
};
/**
* Company Controller
*
* This class is for creating a controller that can manage company data,
* synchronize with external systems, and provide methods for accessing
* and updating company information within our internal system.
* This class manages company data from the local database.
* Data is synced from ConnectWise via the dalpuri service.
*/
export class CompanyController {
public readonly id: string;
public readonly id: number;
public readonly uid: string;
public name: string;
public readonly cw_Identifier: string;
public readonly cw_CompanyId: number;
public cw_Data?: {
company: CWCompany;
defaultContact: Contact | null;
allContacts: Contact[];
};
public phone: string | null;
public website: string | null;
constructor(companyData: Company, cwData?: typeof this.cw_Data) {
private _contacts: Contact[] = [];
private _addresses: CompanyAddress[] = [];
private _defaultContact: Contact | null = null;
private _defaultAddress: CompanyAddress | null = null;
constructor(companyData: CompanyWithRelations) {
this.id = companyData.id;
this.uid = companyData.uid;
this.name = companyData.name;
this.cw_Identifier = companyData.cw_Identifier;
this.cw_CompanyId = companyData.cw_CompanyId;
this.cw_Data = cwData;
this.phone = companyData.phone;
this.website = companyData.website;
if (companyData.contacts) {
this._contacts = companyData.contacts;
this._defaultContact =
companyData.contacts.find((c) => c.default) ?? null;
}
if (companyData.companyAddresses) {
this._addresses = companyData.companyAddresses;
this._defaultAddress =
companyData.companyAddresses.find((a) => a.defaultFlag) ?? null;
}
}
/**
* Hydrate CW Data
* Hydrate Data
*
* Fetches and populates the full ConnectWise company data
* (company, default contact, all contacts) if not already loaded.
*
* @returns {ThisType}
* Loads contacts and addresses from the local database if not already loaded.
*/
public async hydrateCwData() {
if (this.cw_Data) return this;
public async hydrateData() {
if (this._contacts.length === 0) {
this._contacts = await prisma.contact.findMany({
where: { companyId: this.id },
});
this._defaultContact = this._contacts.find((c) => c.default) ?? null;
}
const cwCompany = await fetchCwCompanyById(this.cw_CompanyId);
if (!cwCompany) return this;
const allContactsData = await connectWiseApi.get(
`${cwCompany._info.contacts_href}&pageSize=1000`,
);
// Derive default contact from allContacts instead of a separate CW call
const defaultContactId = cwCompany.defaultContact?.id;
const defaultContactData = defaultContactId
? ((allContactsData.data as any[]).find(
(c: any) => c.id === defaultContactId,
) ?? null)
: null;
this.cw_Data = {
company: cwCompany,
defaultContact: defaultContactData,
allContacts: allContactsData.data,
};
if (this._addresses.length === 0) {
this._addresses = await prisma.companyAddress.findMany({
where: { companyId: this.id },
});
this._defaultAddress = this._addresses.find((a) => a.defaultFlag) ?? null;
}
return this;
}
/**
* Refresh Internal Company Data from ConnectWise
* Refresh from DB
*
* This method fetches the latest company data from ConnectWise and updates
* the internal company information accordingly.
*
* @returns {ThisType} - Updated Controller
* Reloads the company data from the local database.
*/
public async refreshFromCW() {
const data = await updateCwInternalCompany(this.cw_CompanyId);
public async refreshFromDb() {
const data = await prisma.company.findUnique({
where: { id: this.id },
});
if (data) {
this.name = data.name;
this.phone = data.phone;
this.website = data.website;
}
this.name = data?.name || this.name;
return this;
}
/**
* Fetch ConnectWise Company Data
*
* This method retrieves the latest company data directly from ConnectWise
* using the stored ConnectWise Company ID.
*
* @returns {Company}
*/
public async fetchCwData(): Promise<CWCompany | null> {
const data = await fetchCwCompanyById(this.cw_CompanyId);
return data;
}
/**
* Fetch Company Configurations
*
* This method retrieves the configurations associated with
* the company from ConnectWise.
*
* @returns {ProcessedConfiguration}
* Fetches configurations directly from ConnectWise.
*/
public async fetchConfigurations() {
const data = await fetchCompanyConfigurations(this.cw_CompanyId);
return data;
const pageSize = 1000;
const conditions = encodeURIComponent(`company/id=${this.id}`);
const configurations: ConfigurationResponse = [];
try {
for (let page = 1; ; page++) {
const response = await withCwRetry(
() =>
connectWiseApi.get<ConfigurationResponse>(
`/company/configurations?page=${page}&pageSize=${pageSize}&conditions=${conditions}`,
),
{ label: `company-configurations:${this.id}:page-${page}` },
);
const items = Array.isArray(response.data) ? response.data : [];
configurations.push(...items);
if (items.length < pageSize) break;
}
return processConfigurationResponse(configurations);
} catch (error: any) {
const cwStatus = Number(error?.response?.status);
const status = cwStatus >= 400 && cwStatus <= 599 ? cwStatus : 502;
throw new GenericError({
message: `Failed to fetch company configurations from ConnectWise`,
name: "ConnectWiseFetchFailed",
cause:
error?.response?.data?.message ??
error?.response?.statusText ??
error?.message ??
"Unknown ConnectWise error",
status,
});
}
}
/**
* Fetch Company Sites
*
* Retrieves all sites for this company from ConnectWise
* and returns them as serialized site objects.
* Retrieves all sites (addresses) for this company from local DB.
*/
public async fetchSites() {
const sites = await fetchCompanySites(this.cw_CompanyId);
return sites.map(serializeCwSite);
const sites = await prisma.companyAddress.findMany({
where: { companyId: this.id },
});
return sites.map((site) => ({
id: site.id,
name: site.name,
addressLine1: site.addressLine1,
addressLine2: site.addressLine2,
city: site.city,
state: site.state,
zip: site.zipCode,
country: site.country,
phone: site.phone,
fax: site.fax,
defaultFlag: site.defaultFlag,
inactiveFlag: site.inactiveFlag,
}));
}
/**
* 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
* Retrieves a single site by its ID from local DB.
*/
public async fetchSite(cwSiteId: number) {
const site = await fetchCompanySite(this.cw_CompanyId, cwSiteId);
return serializeCwSite(site);
public async fetchSite(siteId: number) {
const site = await prisma.companyAddress.findFirst({
where: { id: siteId, companyId: this.id },
});
if (!site) return null;
return {
id: site.id,
name: site.name,
addressLine1: site.addressLine1,
addressLine2: site.addressLine2,
city: site.city,
state: site.state,
zip: site.zipCode,
country: site.country,
phone: site.phone,
fax: site.fax,
defaultFlag: site.defaultFlag,
inactiveFlag: site.inactiveFlag,
};
}
/**
* Get all contacts
*/
public getContacts() {
return this._contacts;
}
/**
* Get default contact
*/
public getDefaultContact() {
return this._defaultContact;
}
/**
* Get default address
*/
public getDefaultAddress() {
return this._defaultAddress;
}
public toJson(opts?: {
includeAddress: boolean;
includePrimaryContact: boolean;
includeAddress?: boolean;
includePrimaryContact?: boolean;
includeAllContacts?: boolean;
}) {
const cw_Data: Record<string, unknown> = {};
if (opts?.includeAddress) {
cw_Data.address = this._defaultAddress
? {
line1: this._defaultAddress.addressLine1,
line2: this._defaultAddress.addressLine2,
city: this._defaultAddress.city,
state: this._defaultAddress.state,
zip: this._defaultAddress.zipCode,
country: this._defaultAddress.country ?? "US",
}
: null;
}
if (opts?.includePrimaryContact) {
cw_Data.primaryContact = this._defaultContact
? {
firstName: this._defaultContact.firstName,
lastName: this._defaultContact.lastName,
cwId: this._defaultContact.id,
inactive: !this._defaultContact.active,
title: this._defaultContact.title,
phone: this._defaultContact.phone,
email: this._defaultContact.email,
}
: null;
}
if (opts?.includeAllContacts) {
cw_Data.allContacts = this._contacts.map((contact) => ({
firstName: contact.firstName,
lastName: contact.lastName,
cwId: contact.id,
inactive: !contact.active,
title: contact.title,
phone: contact.phone,
email: contact.email,
}));
}
return {
id: this.id,
id: this.uid,
name: this.name,
cw_Identifier: this.cw_Identifier,
cw_CompanyId: this.cw_CompanyId,
cw_Data: {
address: !opts?.includeAddress
? undefined
: {
line1: this.cw_Data?.company.addressLine1,
line2: this.cw_Data?.company.addressLine2 ?? null,
city: this.cw_Data?.company.city,
state: this.cw_Data?.company.state,
zip: this.cw_Data?.company.zip,
country: this.cw_Data?.company.country
? this.cw_Data.company.country.name
: "United States",
},
primaryContact: !opts?.includePrimaryContact
? undefined
: this.cw_Data?.defaultContact
? {
firstName: this.cw_Data.defaultContact.firstName,
lastName: this.cw_Data.defaultContact.lastName,
cwId: this.cw_Data.defaultContact.id,
inactive: this.cw_Data.defaultContact.inactiveFlag,
title: this.cw_Data.defaultContact.title,
phone: this.cw_Data.defaultContact.defaultPhoneNbr,
email: (() => {
if (!this.cw_Data?.defaultContact?.communicationItems)
return null;
return (
this.cw_Data.defaultContact.communicationItems.find(
(v) => v.type.name === "Email",
)?.value ?? null
);
})(),
}
: null,
allContacts: !opts?.includeAllContacts
? undefined
: this.cw_Data?.allContacts.map((contact) => ({
firstName: contact.firstName,
lastName: contact.lastName,
cwId: contact.id,
inactive: contact.inactiveFlag,
title: contact.title,
phone: contact.defaultPhoneNbr,
email: (() => {
if (!contact.communicationItems) return null;
return (
contact.communicationItems.find(
(v) => v.type.name === "Email",
)?.value ?? null
);
})(),
})),
},
cw_CompanyId: this.id,
cw_Data,
};
}
}
-19
View File
@@ -1,5 +1,4 @@
import type { CwMember } from "../../generated/prisma/client";
import type { CWMember } from "../modules/cw-utils/members/fetchAllMembers";
/**
* CW Member Controller
@@ -44,24 +43,6 @@ export class CwMemberController {
return name || this.identifier;
}
/**
* Map CW Member → Prisma create/update payload
*
* Static helper used by both the controller and the refresh sync.
*/
public static mapCwToDb(item: CWMember) {
return {
identifier: item.identifier,
firstName: item.firstName ?? "",
lastName: item.lastName ?? "",
officeEmail: item.officeEmail ?? null,
inactiveFlag: item.inactiveFlag ?? false,
cwLastUpdated: item._info?.lastUpdated
? new Date(item._info.lastUpdated)
: new Date(),
};
}
/**
* To JSON
*
@@ -8,6 +8,9 @@ import { CWForecastItem } from "../modules/cw-utils/opportunities/opportunity.ty
* locally — all data is sourced directly from the ConnectWise API.
*/
export class ForecastProductController {
private static readonly PROCUREMENT_NOTES_FIELD_ID = 29;
private static readonly PRODUCT_NARRATIVE_FIELD_ID = 46;
public readonly cwForecastId: number;
public forecastDescription: string;
@@ -24,6 +27,8 @@ export class ForecastProductController {
public productDescription: string;
public customerDescription: string | null;
public description: string | null;
public procurementNotes: string | null;
public productNarrative: string | null;
public productClass: string;
public forecastType: string;
@@ -49,6 +54,11 @@ export class ForecastProductController {
public cwLastUpdated: Date | null;
public cwUpdatedBy: string | null;
// Pricing data (from local ProductData)
public unitPrice: number;
public listPrice: number;
public discount: number;
// Cancellation data (from procurement products endpoint)
public cancelledFlag: boolean;
public quantityCancelled: number;
@@ -77,8 +87,23 @@ export class ForecastProductController {
this.productDescription = data.productDescription;
this.customerDescription = data.customerDescription ?? null;
this.description = null;
this.procurementNotes =
data.procurementNotes ??
data.customFields
?.find(
(f) => f.id === ForecastProductController.PROCUREMENT_NOTES_FIELD_ID
)
?.value?.toString() ??
null;
this.productNarrative =
data.customFields?.find((f) => f.id === 46)?.value?.toString() ?? null;
data.productNarrative ??
data.customFields
?.find(
(f) => f.id === ForecastProductController.PRODUCT_NARRATIVE_FIELD_ID
)
?.value?.toString() ??
null;
this.productClass = data.productClass;
this.forecastType = data.forecastType;
@@ -105,6 +130,11 @@ export class ForecastProductController {
: null;
this.cwUpdatedBy = data._info?.updatedBy ?? null;
// Pricing defaults — enriched later via applyPricingData()
this.unitPrice = 0;
this.listPrice = 0;
this.discount = 0;
// Cancellation defaults — enriched later via applyCancellationData()
this.cancelledFlag = false;
this.quantityCancelled = 0;
@@ -133,8 +163,19 @@ export class ForecastProductController {
public applyProcurementCustomFields(data: {
customFields?: Array<{ id: number; value?: unknown }>;
}): void {
const notes = data.customFields
?.find(
(f) => f.id === ForecastProductController.PROCUREMENT_NOTES_FIELD_ID
)
?.value?.toString();
if (notes) {
this.procurementNotes = notes;
}
const narrative = data.customFields
?.find((f) => f.id === 46)
?.find(
(f) => f.id === ForecastProductController.PRODUCT_NARRATIVE_FIELD_ID
)
?.value?.toString();
if (narrative) {
this.productNarrative = narrative;
@@ -257,6 +298,7 @@ export class ForecastProductController {
: null,
productDescription: this.productDescription,
customerDescription: this.customerDescription,
procurementNotes: this.procurementNotes,
productNarrative: this.productNarrative,
productClass: this.productClass,
forecastType: this.forecastType,
@@ -281,6 +323,25 @@ export class ForecastProductController {
cwUpdatedBy: this.cwUpdatedBy,
onHand: this.onHand,
inStock: this.inStock,
unitPrice: this.unitPrice,
listPrice: this.listPrice,
discount: this.discount,
};
}
/**
* Apply Pricing Data
*
* Enriches this forecast product with pricing data from the local
* ProductData table.
*/
public applyPricingData(data: {
unitPrice?: number;
listPrice?: number;
discount?: number;
}): void {
this.unitPrice = data.unitPrice ?? 0;
this.listPrice = data.listPrice ?? 0;
this.discount = data.discount ?? 0;
}
}
@@ -33,7 +33,7 @@ export class GeneratedQuoteController {
data: GeneratedQuotes & {
opportunity?: Opportunity | null;
createdBy?: (User & { roles: Role[] }) | null;
},
}
) {
this.id = data.id;
@@ -67,7 +67,7 @@ export class GeneratedQuoteController {
if (this._opportunity) return this._opportunity;
const opportunity = await prisma.opportunity.findFirst({
where: { id: this.opportunityId },
where: { uid: this.opportunityId },
});
if (!opportunity) return null;
@@ -114,8 +114,8 @@ export class GeneratedQuoteController {
quoteFile: !opts?.includeFile
? undefined
: opts?.encodeFileAsBase64
? Buffer.from(this.quoteFile).toString("base64")
: this.quoteFile,
? Buffer.from(this.quoteFile).toString("base64")
: this.quoteFile,
opportunity:
opts?.includeOpportunity && this._opportunity
? this._opportunity.toJson()
File diff suppressed because it is too large Load Diff
+8 -5
View File
@@ -89,7 +89,7 @@ export class RoleController {
});
throw new PermissionsVerificationError(
`Unable to verify permissions for role '${this.title}, it is recommended that you override and rewrite these permissions immediately.`,
(err as Error).message,
(err as Error).message
);
}
@@ -261,14 +261,14 @@ export class RoleController {
title: string;
moniker: string;
permissions: string[];
}>,
}>
) {
const schema = z
.object({
title: z.string().min(1, "Title cannot be empty."),
moniker: z.string().min(1, "Moniker cannot be empty."),
permissions: z.array(
z.string().min(1, "Permission node cannot be empty"),
z.string().min(1, "Permission node cannot be empty")
),
})
.partial()
@@ -284,7 +284,7 @@ export class RoleController {
if (checkMoniker && checkMoniker.moniker !== this.moniker)
throw new RoleError(
"Moniker is already taken.",
"Another role with this moniker already exists in the databse.",
"Another role with this moniker already exists in the databse."
);
}
@@ -337,7 +337,10 @@ export class RoleController {
users: opts?.viewUsers
? this._users.map((v) => ({
id: v.id,
name: v.name,
name:
`${v.firstName ?? ""} ${v.lastName ?? ""}`.trim() ||
v.login ||
v.email,
login: v.login,
roles: v.roles.map((r: any) => r.id),
}))
+84
View File
@@ -0,0 +1,84 @@
import {
Schedule,
ScheduleStatus,
ScheduleType,
ScheduleSpan,
} from "../../generated/prisma/client";
type ScheduleWithRelations = Schedule & {
status?: ScheduleStatus | null;
type?: ScheduleType | null;
scheduleSpan?: ScheduleSpan | null;
};
export class ScheduleController {
public readonly id: number;
public readonly uid: string;
public name: string;
public description: string | null;
private _data: ScheduleWithRelations;
constructor(data: ScheduleWithRelations) {
this.id = data.id;
this.uid = data.uid;
this.name = data.name;
this.description = data.description;
this._data = data;
}
public toJson() {
const d = this._data;
return {
id: d.uid,
cwId: d.id,
memberId: d.memberId,
name: d.name,
description: d.description,
closedFlag: d.closedFlag,
reminderFlag: d.reminderFlag,
allDayFlag: d.allDayFlag,
acknowledgementFlag: d.acknowledgementFlag,
meetingFlag: d.meetingFlag,
recurringFlag: d.recurringFlag,
billableFlag: d.billableFlag,
acknowledgedById: d.acknowledgedById,
acknowledgedAt: d.acknowledgedAt,
startDate: d.startDate,
endDate: d.endDate,
hoursScheduled: d.hoursScheduled,
duration: d.duration,
hoursPerDay: d.hoursPerDay,
reminderMinutes: d.reminderMinutes,
closedById: d.closedById,
closedAt: d.closedAt,
createdById: d.createdById,
updatedById: d.updatedById,
createdAt: d.createdAt,
updatedAt: d.updatedAt,
status: d.status
? {
id: d.status.id,
uid: d.status.uid,
name: d.status.name,
color: d.status.color,
}
: null,
type: d.type
? {
id: d.type.id,
uid: d.type.uid,
name: d.type.name,
displayColor: d.type.displayColor,
}
: null,
scheduleSpan: d.scheduleSpan
? {
id: d.scheduleSpan.id,
scheduleSpanId: d.scheduleSpan.scheduleSpanId,
spanDesc: d.scheduleSpan.spanDesc,
}
: null,
};
}
}
+61 -14
View File
@@ -1,6 +1,5 @@
import { Collection } from "@discordjs/collection";
import { Role } from "../../generated/prisma/client";
import { User } from "../../generated/prisma/browser";
import { Role, User } from "../../generated/prisma/client";
import { SessionTokensObject } from "./SessionController";
import { sessions } from "../managers/sessions";
import BodyError from "../Errors/BodyError";
@@ -15,11 +14,13 @@ import { permissionsPrivateKey } from "../constants";
export default class UserController {
public id: string;
public name: string | null;
public firstName: string | null;
public lastName: string | null;
public login: string;
public email: string;
public image: string | null;
public cwIdentifier: string | null;
public cwMemberId: number | null;
private _roles: Collection<string, Role>;
private _permissions: string | null;
@@ -33,13 +34,38 @@ export default class UserController {
public createdAt: Date;
public updatedAt: Date;
public get name(): string | null {
const full = [this.firstName, this.lastName]
.filter(Boolean)
.join(" ")
.trim();
return full.length > 0 ? full : null;
}
private _splitName(name: string): {
firstName: string | null;
lastName: string | null;
} {
const trimmed = name.trim();
if (!trimmed) return { firstName: null, lastName: null };
const parts = trimmed.split(/\s+/);
const firstName = parts.shift() ?? null;
const lastName = parts.length > 0 ? parts.join(" ") : null;
return { firstName, lastName };
}
constructor(userdata: User & { roles: Role[] }) {
this.id = userdata.id;
this.name = userdata.name;
this.firstName = userdata.firstName ?? null;
this.lastName = userdata.lastName ?? null;
this.login = userdata.login;
this.email = userdata.email;
this.image = userdata.image;
this.cwIdentifier = userdata.cwIdentifier ?? null;
this.cwMemberId = userdata.cwMemberId ?? null;
this.updatedAt = userdata.updatedAt;
this.createdAt = userdata.createdAt;
this._permissions = userdata.permissions ?? null;
@@ -62,11 +88,13 @@ export default class UserController {
*/
private _updateInternalValues(userdata: User) {
this.id = userdata.id;
this.name = userdata.name;
this.firstName = userdata.firstName ?? null;
this.lastName = userdata.lastName ?? null;
this.login = userdata.login;
this.email = userdata.email;
this.image = userdata.image;
this.cwIdentifier = userdata.cwIdentifier ?? null;
this.cwMemberId = userdata.cwMemberId ?? null;
this.updatedAt = userdata.updatedAt;
this.createdAt = userdata.createdAt;
}
@@ -92,17 +120,33 @@ export default class UserController {
* @param data - A partial of the user data
* @returns {Promise<UserController>} - The updated user controller
*/
public async update(data: Partial<Pick<User, "name" | "image">>) {
if (Object.keys(data).length == 0)
public async update(
data: Partial<Pick<User, "firstName" | "lastName" | "image">> & {
name?: string;
}
) {
const updateData: Partial<Pick<User, "firstName" | "lastName" | "image">> =
{};
if (data.image !== undefined) updateData.image = data.image;
if (data.name !== undefined) {
const parsed = this._splitName(data.name);
updateData.firstName = parsed.firstName;
updateData.lastName = parsed.lastName;
}
if (data.firstName !== undefined) updateData.firstName = data.firstName;
if (data.lastName !== undefined) updateData.lastName = data.lastName;
if (Object.keys(updateData).length == 0)
throw new BodyError("Body cannot be empty.");
const updatedUser = await prisma.user.update({
where: { id: this.id },
data,
data: updateData,
});
this._updateInternalValues(updatedUser);
events.emit("user:updated", { user: this, updatedValues: data });
events.emit("user:updated", { user: this, updatedValues: updateData });
return this;
}
@@ -118,7 +162,7 @@ export default class UserController {
*/
public async setRoles(roleIdentifiers: string[]): Promise<UserController> {
const resolvedRoles = await Promise.all(
roleIdentifiers.map((identifier) => roles.fetch(identifier)),
roleIdentifiers.map((identifier) => roles.fetch(identifier))
);
const updatedUser = await prisma.user.update({
@@ -242,8 +286,8 @@ export default class UserController {
await Promise.all(
this._roles.map(async (v) =>
collection.set(v.id, await roles.fetch(v.id)),
),
collection.set(v.id, await roles.fetch(v.id))
)
);
return collection;
@@ -307,11 +351,13 @@ export default class UserController {
return {
id: this.id,
name: this.name,
firstName: this.firstName,
lastName: this.lastName,
roles: opts?.safeReturn
? undefined
: this._roles.size > 0
? this._roles.map((v) => v.moniker)
: undefined,
? this._roles.map((v) => v.moniker)
: undefined,
permissions: opts?.safeReturn
? undefined
: (() => {
@@ -325,6 +371,7 @@ export default class UserController {
login: opts?.safeReturn ? undefined : this.login,
email: opts?.safeReturn ? undefined : this.email,
cwIdentifier: opts?.safeReturn ? undefined : this.cwIdentifier,
cwMemberId: this.cwMemberId,
image: this.image,
createdAt: this.createdAt,
updatedAt: this.updatedAt,