Lots of updates and cleaning up.
This commit is contained in:
@@ -0,0 +1,73 @@
|
|||||||
|
# Copilot / AI Agent Instructions for ttscm-api
|
||||||
|
|
||||||
|
Purpose: make AI coding agents immediately productive in this repository by describing architecture, conventions, workflows, and helpful code pointers.
|
||||||
|
|
||||||
|
-- **Big picture**: This is a TypeScript API service (runs on Bun) using the Hono framework. The HTTP surface is implemented in `src/api` where small router files are mounted on a versioned router in `src/api/server.ts` (see the `/v1` mount). Typical request flow is:
|
||||||
|
|
||||||
|
- `router` (in `src/api/routers/*`) → `controller` (in `src/controllers/*`) → `manager` (in `src/managers/*`) → `module` / `generated/prisma` for persistence and external integrations.
|
||||||
|
- Keep each layer focused: routers handle routing & middleware, controllers handle request validation and high-level orchestration, managers encapsulate domain/persistence logic, modules provide shared utilities (external API clients, helpers).
|
||||||
|
|
||||||
|
- **Runtime / tooling**: The project runs on Bun. Dev command: `npm run dev` (runs `bun --watch src/index.ts`). DB tooling uses Prisma; generated client lives under `generated/prisma` (do NOT edit generated files). Key scripts in `package.json`:
|
||||||
|
- `dev` — start the server with Bun in watch mode
|
||||||
|
- `db:gen` — `prisma generate`
|
||||||
|
- `db:push` — `prisma migrate dev --skip-generate`
|
||||||
|
|
||||||
|
- **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` in browser contexts). Always run the `db:gen` script after updating `schema.prisma`.
|
||||||
|
|
||||||
|
- **Routing & controllers**: Example flow: a request hits a `router` in `src/api/routers/*` → router delegates to a `controller` in `src/controllers/*` → controller calls a `manager` in `src/managers/*` for domain/persistence logic. Important concrete patterns:
|
||||||
|
- `src/api/server.ts` mounts `v1` and uses `v1.route("/auth", require("./routers/authRouter").default)` style requires; preserve this shape when adding routes.
|
||||||
|
- Router files export a default Hono router object (CommonJS `module.exports`/`export default` mixture is used across the codebase).
|
||||||
|
- Controllers are single-export modules named like `CompanyController.ts` with named methods per action (e.g., `fetch`, `update`). Prefer small methods that call into `managers/*`.
|
||||||
|
- Managers are thin domain layers (e.g., `managers/companies.ts`) that wrap `generated/prisma` calls and other I/O. Keep side effects here, keep controllers pure orchestration.
|
||||||
|
- Use `src/modules/api-utils/apiResponse.ts` for every HTTP response shape (successful/created/error/internalError/zodError).
|
||||||
|
- Use Zod schemas in controllers for request validation; server-level `app.onError` maps `ZodError` to `apiResponse.zodError`.
|
||||||
|
|
||||||
|
**API layout & conventions (how to add a new endpoint)**
|
||||||
|
|
||||||
|
- **Add router**: create `src/api/routers/<thing>Router.ts` exporting a Hono router and mount it in `src/api/server.ts` under the `v1` router.
|
||||||
|
- **Add controller**: create `src/controllers/<Thing>Controller.ts` exporting functions for each action. Controllers should validate input with Zod, call managers, and return `apiResponse.*` results.
|
||||||
|
- **Add manager**: create `src/managers/<things>.ts` for persistence/domain logic. Use the generated Prisma client (`generated/prisma/client.ts`) here; do not import Prisma directly in controllers.
|
||||||
|
- **Add modules/types**: if needed, add helpers to `src/modules/*` and runtime types to `src/types/*`.
|
||||||
|
- **Middleware & auth**: use `src/api/middleware/authorization.ts` for protecting routes; follow existing token/session patterns from `src/controllers/SessionController.ts` and `src/Errors/*`.
|
||||||
|
- **Error handling**: throw repository-specific errors from `src/Errors/*` (include `status`, `name`, `message`, optional `cause`) and let `src/api/server.ts` map them via `apiResponse.error`.
|
||||||
|
- **Naming**: prefer plural manager filenames (`companies.ts`) and singular controller names (`CompanyController.ts`) — follow existing files.
|
||||||
|
|
||||||
|
**Examples & notable files**
|
||||||
|
|
||||||
|
- `src/api/server.ts` — mounts `v1`, registers `cors`, central `onError` handler and `notFound` response.
|
||||||
|
- `src/api/routers/companyRouter.ts` and `src/controllers/CompanyController.ts` — canonical example for adding company endpoints.
|
||||||
|
- `src/api/user/@me/*` — nested route example (use Hono sub-routers for subpaths).
|
||||||
|
- `src/modules/cw-utils/*` — external API integrations; keep interfaces stable and return domain objects consumed by managers.
|
||||||
|
|
||||||
|
- **Validation & errors**: Zod is used for input validation; Zod errors are handled centrally in `src/api/server.ts` via `apiResponse.zodError`. Application errors use custom error classes in `src/Errors/*` (e.g., `AuthenticationError.ts`, `GenericError.ts`). When creating errors, follow the shape used in existing errors (include `status`, `name`, `message`, and optional `cause`).
|
||||||
|
|
||||||
|
- **Response pattern**: Use the `apiResponse` helpers in `src/modules/api-utils/apiResponse.ts` for formatting responses and status codes (successful, created, error, internalError, zodError).
|
||||||
|
|
||||||
|
- **Auth & external integrations**: Microsoft OAuth flow is under `src/api/auth/*` and `src/modules/fetchMicrosoftUser.ts`. If touching authentication, follow existing redirect/refresh patterns in `src/api/auth` and preserve token-refresh semantics.
|
||||||
|
|
||||||
|
- **ConnectWise integration**: Utilities for ConnectWise interactions live in `src/modules/cw-utils/*` (e.g., `fetchCompanyConfigurations.ts`). These modules often call external APIs and return domain data; preserve the module contracts (input types and returned shapes) when refactoring.
|
||||||
|
|
||||||
|
- **Generated files and CI**: `generated/` is a build artifact. Do not modify. When updating Prisma models, run `npm run db:gen` and commit changes to `generated/prisma` only if that's the established workflow.
|
||||||
|
|
||||||
|
- **Files to inspect for context / examples**:
|
||||||
|
- `src/api/server.ts` — central error handling, router mounting, CORS and not-found handling.
|
||||||
|
- `src/modules/api-utils/apiResponse.ts` — response shaping used across controllers.
|
||||||
|
- `src/controllers/CompanyController.ts` — example controller calling managers.
|
||||||
|
- `src/modules/cw-utils/fetchCompanyConfigurations.ts` — example external integration utility.
|
||||||
|
- `generated/prisma/client.ts` — generated Prisma client imports; avoid editing.
|
||||||
|
|
||||||
|
- **Coding conventions & patterns specific to this repo**:
|
||||||
|
- Prefer the existing layered architecture: routers → controllers → managers → modules.
|
||||||
|
- Use the `apiResponse` helpers for all HTTP responses.
|
||||||
|
- Throw or propagate repository-specific custom errors (from `src/Errors/*`) rather than returning bare objects.
|
||||||
|
- Keep TypeScript types in `src/types/*` and use Zod for runtime checks.
|
||||||
|
- **Avoid `else` statements** — prefer ternary operators, early returns, or other control flow patterns. Only use `else` if there is absolutely no other way.
|
||||||
|
|
||||||
|
- **Local dev / quick checks**:
|
||||||
|
- Start dev server: `npm run dev`
|
||||||
|
- Regenerate Prisma client: `npm run db:gen`
|
||||||
|
- Apply DB migrations locally: `npm run db:push`
|
||||||
|
|
||||||
|
- **When editing generated or infra files**: if you need to change `generated/prisma/*` (rare), explain why in the PR and show commands used to regenerate.
|
||||||
|
|
||||||
|
If anything here is unclear or you'd like more examples (e.g., a walk-through editing a controller + manager + test run), tell me which area to expand and I'll iterate.
|
||||||
@@ -23,8 +23,6 @@ export default createRoute("get", ["/redirect"], async (c) => {
|
|||||||
refreshToken: tokens.refreshToken,
|
refreshToken: tokens.refreshToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Emitted auth callback for key:", callbackKey);
|
|
||||||
|
|
||||||
// Close the window because duh
|
// Close the window because duh
|
||||||
return c.html(
|
return c.html(
|
||||||
`<script>
|
`<script>
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ export default createRoute("post", ["/refresh"], async (c) => {
|
|||||||
|
|
||||||
const refreshToken = c.req.header("x-refresh-token") || "";
|
const refreshToken = c.req.header("x-refresh-token") || "";
|
||||||
|
|
||||||
console.log("Received refresh token:", refreshToken);
|
|
||||||
|
|
||||||
const session = await sessions.fetch({
|
const session = await sessions.fetch({
|
||||||
refreshToken: refreshToken,
|
refreshToken: refreshToken,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Hono } from "hono/tiny";
|
||||||
|
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||||
|
import { companies } from "../../../managers/companies";
|
||||||
|
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||||
|
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { authMiddleware } from "../../middleware/authorization";
|
||||||
|
|
||||||
|
/* /v1/company/companies/[id]/configurations */
|
||||||
|
export default createRoute(
|
||||||
|
"get",
|
||||||
|
["/companies/:identifier/configurations"],
|
||||||
|
|
||||||
|
async (c) => {
|
||||||
|
const company = await companies.fetch(c.req.param("identifier"));
|
||||||
|
const configurations = await company.fetchConfigurations();
|
||||||
|
|
||||||
|
const response = apiResponse.successful(
|
||||||
|
"Company Configurations Fetched Successfully!",
|
||||||
|
configurations,
|
||||||
|
);
|
||||||
|
return c.json(response, response.status as ContentfulStatusCode);
|
||||||
|
},
|
||||||
|
authMiddleware({
|
||||||
|
permissions: ["company.fetch", "company.fetch.configurations"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
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 { companies } from "../../managers/companies";
|
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";
|
||||||
|
|
||||||
/* /v1/company/[id] */
|
/* /v1/company/companies/[id] */
|
||||||
export default createRoute(
|
export default createRoute(
|
||||||
"get",
|
"get",
|
||||||
["/company/:identifier"],
|
["/companies/:identifier"],
|
||||||
|
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const company = await companies.fetch(c.req.param("identifier"));
|
const company = await companies.fetch(c.req.param("identifier"));
|
||||||
|
|
||||||
@@ -12,9 +12,15 @@ export default createRoute(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const page = new Number(c.req.query("page") ?? 1) as number;
|
const page = new Number(c.req.query("page") ?? 1) as number;
|
||||||
const rpp = new Number(c.req.query("rpp") ?? 30) as number; // Records Per Page
|
const rpp = new Number(c.req.query("rpp") ?? 30) as number; // Records Per Page
|
||||||
const companyQty = await companies.count();
|
const search = c.req.query("search") as string;
|
||||||
|
|
||||||
const data = await companies.fetchPages(page, rpp);
|
const data = search
|
||||||
|
? await companies.search(search, page, rpp)
|
||||||
|
: await companies.fetchPages(page, rpp);
|
||||||
|
|
||||||
|
const companyQty = search
|
||||||
|
? (await companies.search(search, 1, 999999)).length
|
||||||
|
: await companies.count();
|
||||||
|
|
||||||
let response = apiResponse.successful(
|
let response = apiResponse.successful(
|
||||||
"Companies Fetched Successfully!",
|
"Companies Fetched Successfully!",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { default as fetchAll } from "./fetchAll";
|
import { default as fetchAll } from "./fetchAll";
|
||||||
import { default as fetch } from "./fetch";
|
import { default as fetch } from "./[id]/fetch";
|
||||||
|
import { default as configurations } from "./[id]/configurations";
|
||||||
|
|
||||||
export { fetch, fetchAll };
|
export { configurations, fetch, fetchAll };
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ app.notFound((c) => {
|
|||||||
v1.route("/teapot", teapot);
|
v1.route("/teapot", teapot);
|
||||||
v1.route("/auth", require("./routers/authRouter").default);
|
v1.route("/auth", require("./routers/authRouter").default);
|
||||||
v1.route("/user", require("./routers/user").default);
|
v1.route("/user", require("./routers/user").default);
|
||||||
v1.route("/", require("./routers/companyRouter").default);
|
v1.route("/company", require("./routers/companyRouter").default);
|
||||||
app.route("/v1", v1);
|
app.route("/v1", v1);
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Company } from "../../generated/prisma/client";
|
import { Company } from "../../generated/prisma/client";
|
||||||
|
import { fetchCwCompanyById } from "../modules/cw-utils/fetchCompany";
|
||||||
|
import { fetchCompanyConfigurations } from "../modules/cw-utils/fetchCompanyConfigurations";
|
||||||
import { updateCwInternalCompany } from "../modules/cw-utils/updateCompany";
|
import { updateCwInternalCompany } from "../modules/cw-utils/updateCompany";
|
||||||
|
import { Company as CWCompany } from "../types/ConnectWiseTypes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Company Controller
|
* Company Controller
|
||||||
@@ -36,6 +39,32 @@ export class CompanyController {
|
|||||||
return this;
|
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}
|
||||||
|
*/
|
||||||
|
public async fetchConfigurations() {
|
||||||
|
const data = await fetchCompanyConfigurations(this.cw_CompanyId);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
public toJson() {
|
public toJson() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
|||||||
@@ -138,12 +138,11 @@ export class RoleController {
|
|||||||
data: { permissions: newPermissionsToken },
|
data: { permissions: newPermissionsToken },
|
||||||
});
|
});
|
||||||
|
|
||||||
events.emit("role:permissions:updated", {
|
events.emit("role:permissions:set", {
|
||||||
current: permissions,
|
current: permissions,
|
||||||
currentSigned: newPermissionsToken,
|
currentSigned: newPermissionsToken,
|
||||||
previous: previous.permissions,
|
previous: previous.permissions,
|
||||||
previousSigned: this._permissionsToken,
|
previousSigned: this._permissionsToken,
|
||||||
action: "set",
|
|
||||||
role: this,
|
role: this,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -178,12 +177,11 @@ export class RoleController {
|
|||||||
data: { permissions: newPermissionsToken },
|
data: { permissions: newPermissionsToken },
|
||||||
});
|
});
|
||||||
|
|
||||||
events.emit("role:permissions:updated", {
|
events.emit("role:permissions:added", {
|
||||||
current: permissions,
|
|
||||||
currentSigned: newPermissionsToken,
|
|
||||||
previous: previous.permissions,
|
previous: previous.permissions,
|
||||||
previousSigned: this._permissionsToken,
|
previousSigned: this._permissionsToken,
|
||||||
action: "added",
|
added: permissions,
|
||||||
|
currentSigned: newPermissionsToken,
|
||||||
role: this,
|
role: this,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -218,12 +216,11 @@ export class RoleController {
|
|||||||
data: { permissions: newPermissionsToken },
|
data: { permissions: newPermissionsToken },
|
||||||
});
|
});
|
||||||
|
|
||||||
events.emit("role:permissions:updated", {
|
events.emit("role:permissions:removed", {
|
||||||
current: permissions,
|
|
||||||
currentSigned: newPermissionsToken,
|
|
||||||
previous: previous.permissions,
|
previous: previous.permissions,
|
||||||
previousSigned: this._permissionsToken,
|
previousSigned: this._permissionsToken,
|
||||||
action: "removed",
|
removed: permissions,
|
||||||
|
currentSigned: newPermissionsToken,
|
||||||
role: this,
|
role: this,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -156,8 +156,6 @@ export default class UserController {
|
|||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// console.log(implicitPermissions);
|
|
||||||
|
|
||||||
let checks = [
|
let checks = [
|
||||||
(await this.fetchRoles()).map((v) => v.checkPermission(permission)),
|
(await this.fetchRoles()).map((v) => v.checkPermission(permission)),
|
||||||
].flatMap((v) => v);
|
].flatMap((v) => v);
|
||||||
|
|||||||
@@ -40,4 +40,35 @@ export const companies = {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search Companies
|
||||||
|
*
|
||||||
|
* Search companies by identifier, name, or id with pagination support.
|
||||||
|
*
|
||||||
|
* @param query - Search query string
|
||||||
|
* @param page - Page number
|
||||||
|
* @param rpp - Records Per Page
|
||||||
|
*/
|
||||||
|
async search(query: string, page: number, rpp: number) {
|
||||||
|
page = page.valueOf();
|
||||||
|
rpp = rpp.valueOf();
|
||||||
|
|
||||||
|
const skip = (page > 1 ? page : 0) * rpp;
|
||||||
|
const take = rpp ?? 30;
|
||||||
|
|
||||||
|
const data = prisma.company.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ cw_Identifier: { contains: query, mode: "insensitive" } },
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ id: { contains: query, mode: "insensitive" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { connectWiseApi } from "../../constants";
|
||||||
|
import { ConfigurationResponse } from "../../types/ConnectWiseTypes";
|
||||||
|
import {
|
||||||
|
processConfigurationResponse,
|
||||||
|
ProcessedConfiguration,
|
||||||
|
} from "./processConfigurationResponse";
|
||||||
|
import GenericError from "../../Errors/GenericError";
|
||||||
|
|
||||||
|
export const fetchCompanyConfigurations = async (
|
||||||
|
cwCompanyId: number,
|
||||||
|
): Promise<ProcessedConfiguration> => {
|
||||||
|
try {
|
||||||
|
const response = await connectWiseApi.get(
|
||||||
|
`/company/configurations?conditions=company/id=${cwCompanyId}`,
|
||||||
|
);
|
||||||
|
return processConfigurationResponse(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
const errBody = (error as any).response?.data || error;
|
||||||
|
console.error(
|
||||||
|
`Error fetching configurations for company ID ${cwCompanyId}:`,
|
||||||
|
errBody,
|
||||||
|
);
|
||||||
|
throw new GenericError({
|
||||||
|
name: "FetchCompanyConfigurationsError",
|
||||||
|
message: `Failed to fetch configurations for company ${cwCompanyId}`,
|
||||||
|
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||||
|
status: 502,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { ConfigurationResponse } from "../../types/ConnectWiseTypes";
|
||||||
|
|
||||||
|
export type ProcessedConfiguration = ReturnType<
|
||||||
|
typeof processConfigurationResponse
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const processConfigurationResponse = (c: ConfigurationResponse) => {
|
||||||
|
return c.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
active: item.activeFlag,
|
||||||
|
serialNumber: item.serialNumber,
|
||||||
|
type: item.type,
|
||||||
|
questions: item.questions.map((q) => ({
|
||||||
|
id: q.questionId,
|
||||||
|
question: q.question,
|
||||||
|
answer: q.answer,
|
||||||
|
fieldType: q.fieldType,
|
||||||
|
})),
|
||||||
|
info: item._info,
|
||||||
|
}));
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { events } from "../globalEvents";
|
|||||||
import { fetchAllCwCompanies } from "./fetchAllCompanies";
|
import { fetchAllCwCompanies } from "./fetchAllCompanies";
|
||||||
|
|
||||||
export const refreshCompanies = async () => {
|
export const refreshCompanies = async () => {
|
||||||
events.emit("cw:companies:refreshed:check");
|
events.emit("cw:companies:refresh:check");
|
||||||
|
|
||||||
// Dynamically import to avoid circular dependency
|
// Dynamically import to avoid circular dependency
|
||||||
const internalCompanyCount = await prisma.company.count();
|
const internalCompanyCount = await prisma.company.count();
|
||||||
@@ -11,28 +11,35 @@ export const refreshCompanies = async () => {
|
|||||||
(await connectWiseApi.get("/company/companies/count")).data.count - 1;
|
(await connectWiseApi.get("/company/companies/count")).data.count - 1;
|
||||||
|
|
||||||
if (internalCompanyCount !== externalCompanyCount) {
|
if (internalCompanyCount !== externalCompanyCount) {
|
||||||
console.log(
|
events.emit("cw:companies:refresh:started");
|
||||||
`Company count mismatch detected. Internal: ${internalCompanyCount}, External: ${externalCompanyCount}. Refreshing...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const allCompanies = await fetchAllCwCompanies();
|
const allCompanies = await fetchAllCwCompanies();
|
||||||
|
|
||||||
await Promise.all(
|
const updatedCount = (
|
||||||
allCompanies.map(async (company) => {
|
await Promise.all(
|
||||||
return await prisma.company.upsert({
|
allCompanies.map(async (company) => {
|
||||||
where: { cw_CompanyId: company.id },
|
return await prisma.company.upsert({
|
||||||
create: {
|
where: { cw_CompanyId: company.id },
|
||||||
cw_CompanyId: company.id,
|
create: {
|
||||||
cw_Identifier: company.identifier,
|
cw_CompanyId: company.id,
|
||||||
name: company.name,
|
cw_Identifier: company.identifier,
|
||||||
},
|
name: company.name,
|
||||||
update: {
|
},
|
||||||
name: company.name,
|
update: {
|
||||||
},
|
name: company.name,
|
||||||
});
|
},
|
||||||
}),
|
});
|
||||||
);
|
}),
|
||||||
events.emit("cw:companies:refreshed", {
|
)
|
||||||
|
).length;
|
||||||
|
|
||||||
|
events.emit("cw:companies:refresh:completed", {
|
||||||
|
internalCompaniesCount: internalCompanyCount,
|
||||||
|
externalCompaniesCount: externalCompanyCount,
|
||||||
|
companiesUpdated: updatedCount,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
events.emit("cw:companies:refresh:skipped", {
|
||||||
internalCompaniesCount: internalCompanyCount,
|
internalCompaniesCount: internalCompanyCount,
|
||||||
externalCompaniesCount: externalCompanyCount,
|
externalCompaniesCount: externalCompanyCount,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import {
|
|||||||
SessionTokensObject,
|
SessionTokensObject,
|
||||||
} from "../controllers/SessionController";
|
} from "../controllers/SessionController";
|
||||||
import { RoleController } from "../controllers/RoleController";
|
import { RoleController } from "../controllers/RoleController";
|
||||||
|
import { CompanyController } from "../controllers/CompanyController";
|
||||||
import { JsonWebTokenError } from "jsonwebtoken";
|
import { JsonWebTokenError } from "jsonwebtoken";
|
||||||
import { User } from "../../generated/prisma/client";
|
import { User, Company } from "../../generated/prisma/client";
|
||||||
|
|
||||||
interface EventTypes {
|
interface EventTypes {
|
||||||
|
// API Lifecycle
|
||||||
"api:started": () => void;
|
"api:started": () => void;
|
||||||
|
|
||||||
|
// User Events
|
||||||
"user:created": (user: UserController) => void;
|
"user:created": (user: UserController) => void;
|
||||||
"user:updated": (data: {
|
"user:updated": (data: {
|
||||||
user: UserController;
|
user: UserController;
|
||||||
@@ -19,6 +23,16 @@ interface EventTypes {
|
|||||||
user: UserController;
|
user: UserController;
|
||||||
tokens: SessionTokensObject;
|
tokens: SessionTokensObject;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
"user:role:assigned": (data: {
|
||||||
|
user: UserController;
|
||||||
|
role: RoleController;
|
||||||
|
}) => void;
|
||||||
|
"user:role:removed": (data: {
|
||||||
|
user: UserController;
|
||||||
|
role: RoleController;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
// Session Events
|
||||||
"session:created": (data: {
|
"session:created": (data: {
|
||||||
user: UserController;
|
user: UserController;
|
||||||
session: SessionController;
|
session: SessionController;
|
||||||
@@ -33,18 +47,33 @@ interface EventTypes {
|
|||||||
}) => void;
|
}) => void;
|
||||||
"session:invalidated": (session: SessionController) => void;
|
"session:invalidated": (session: SessionController) => void;
|
||||||
"session:terminated": (session: SessionController) => void;
|
"session:terminated": (session: SessionController) => void;
|
||||||
|
|
||||||
|
// Role Events
|
||||||
"role:created": (role: RoleController) => void;
|
"role:created": (role: RoleController) => void;
|
||||||
"role:deleted": (role: RoleController) => void;
|
"role:deleted": (role: RoleController) => void;
|
||||||
"role:updated": (data: {
|
"role:updated": (data: {
|
||||||
role: RoleController;
|
role: RoleController;
|
||||||
updateData: Parameters<typeof RoleController.prototype.update>["0"];
|
updateData: Parameters<typeof RoleController.prototype.update>["0"];
|
||||||
}) => void;
|
}) => void;
|
||||||
"role:permissions:updated": (data: {
|
"role:permissions:set": (data: {
|
||||||
previous: string[];
|
previous: string[];
|
||||||
previousSigned: string;
|
previousSigned: string;
|
||||||
current: string[];
|
current: string[];
|
||||||
currentSigned: string;
|
currentSigned: string;
|
||||||
action: "set" | "added" | "removed";
|
role: RoleController;
|
||||||
|
}) => void;
|
||||||
|
"role:permissions:added": (data: {
|
||||||
|
previous: string[];
|
||||||
|
previousSigned: string;
|
||||||
|
added: string[];
|
||||||
|
currentSigned: string;
|
||||||
|
role: RoleController;
|
||||||
|
}) => void;
|
||||||
|
"role:permissions:removed": (data: {
|
||||||
|
previous: string[];
|
||||||
|
previousSigned: string;
|
||||||
|
removed: string[];
|
||||||
|
currentSigned: string;
|
||||||
role: RoleController;
|
role: RoleController;
|
||||||
}) => void;
|
}) => void;
|
||||||
"role:permissions:verification_error": (data: {
|
"role:permissions:verification_error": (data: {
|
||||||
@@ -53,10 +82,30 @@ interface EventTypes {
|
|||||||
err: Error;
|
err: Error;
|
||||||
role: RoleController;
|
role: RoleController;
|
||||||
}) => void;
|
}) => void;
|
||||||
"cw:companies:refreshed:check": () => void;
|
|
||||||
"cw:companies:refreshed": (data: {
|
// Company Events
|
||||||
|
"company:fetched": (company: CompanyController) => void;
|
||||||
|
"company:refreshed_from_cw": (company: CompanyController) => void;
|
||||||
|
"company:configurations_fetched": (data: {
|
||||||
|
company: CompanyController;
|
||||||
|
configurationCount: number;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
// ConnectWise Integration Events
|
||||||
|
"cw:companies:refresh:check": () => void;
|
||||||
|
"cw:companies:refresh:started": () => void;
|
||||||
|
"cw:companies:refresh:completed": (data: {
|
||||||
internalCompaniesCount: number;
|
internalCompaniesCount: number;
|
||||||
externalCompaniesCount: number;
|
externalCompaniesCount: number;
|
||||||
|
companiesUpdated: number;
|
||||||
|
}) => void;
|
||||||
|
"cw:companies:refresh:skipped": (data: {
|
||||||
|
internalCompaniesCount: number;
|
||||||
|
externalCompaniesCount: number;
|
||||||
|
}) => void;
|
||||||
|
"cw:company:data:updated": (data: {
|
||||||
|
company: CompanyController;
|
||||||
|
updatedFields: Partial<Company>;
|
||||||
}) => void;
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,3 +137,169 @@ export interface CustomField {
|
|||||||
userDefinedFieldRecId: number;
|
userDefinedFieldRecId: number;
|
||||||
podId: string;
|
podId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* CONFIGURATION INTERFACES
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ConfigurationItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
type: ConfigurationTypeRef;
|
||||||
|
status: ConfigurationStatusRef;
|
||||||
|
|
||||||
|
company: CompanyRef;
|
||||||
|
contact: ContactRef;
|
||||||
|
site: SiteRef;
|
||||||
|
|
||||||
|
locationId: number;
|
||||||
|
location: LocationRef;
|
||||||
|
|
||||||
|
businessUnitId: number;
|
||||||
|
department: DepartmentRef;
|
||||||
|
|
||||||
|
deviceIdentifier: string;
|
||||||
|
serialNumber: string;
|
||||||
|
modelNumber: string;
|
||||||
|
tagNumber: string;
|
||||||
|
|
||||||
|
installationDate: string; // ISO-8601
|
||||||
|
warrantyExpirationDate?: string; // ISO-8601 (only present on some items)
|
||||||
|
|
||||||
|
installedBy?: MemberRef; // only present on some items
|
||||||
|
|
||||||
|
vendorNotes: string;
|
||||||
|
notes: string;
|
||||||
|
|
||||||
|
macAddress: string;
|
||||||
|
lastLoginName: string;
|
||||||
|
|
||||||
|
billFlag: boolean;
|
||||||
|
|
||||||
|
backupSuccesses: number;
|
||||||
|
backupIncomplete: number;
|
||||||
|
backupFailed: number;
|
||||||
|
backupRestores: number;
|
||||||
|
backupServerName: string;
|
||||||
|
backupBillableSpaceGb: number;
|
||||||
|
backupProtectedDeviceList: string;
|
||||||
|
backupYear: number;
|
||||||
|
backupMonth: number;
|
||||||
|
|
||||||
|
ipAddress: string;
|
||||||
|
defaultGateway: string;
|
||||||
|
|
||||||
|
osType: string;
|
||||||
|
osInfo: string;
|
||||||
|
cpuSpeed: string;
|
||||||
|
ram: string;
|
||||||
|
localHardDrives: string;
|
||||||
|
|
||||||
|
questions: ConfigurationQuestion[];
|
||||||
|
|
||||||
|
activeFlag: boolean;
|
||||||
|
mobileGuid: string;
|
||||||
|
|
||||||
|
companyLocationId: number;
|
||||||
|
showRemoteFlag: boolean;
|
||||||
|
showAutomateFlag: boolean;
|
||||||
|
needsRenewalFlag: boolean;
|
||||||
|
|
||||||
|
manufacturerPartNumber?: string; // only present on some items
|
||||||
|
|
||||||
|
_info: ConfigurationInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigurationTypeRef {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
_info: {
|
||||||
|
type_href: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigurationStatusRef {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
_info: {
|
||||||
|
status_href: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompanyRef {
|
||||||
|
id: number;
|
||||||
|
identifier: string;
|
||||||
|
name: string;
|
||||||
|
_info: {
|
||||||
|
company_href: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContactRef {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
_info: {
|
||||||
|
contact_href: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SiteRef {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
_info: {
|
||||||
|
site_href: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationRef {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
_info: {
|
||||||
|
location_href: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DepartmentRef {
|
||||||
|
id: number;
|
||||||
|
identifier: string;
|
||||||
|
name: string;
|
||||||
|
_info: {
|
||||||
|
department_href: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemberRef {
|
||||||
|
id: number;
|
||||||
|
identifier: string;
|
||||||
|
name: string;
|
||||||
|
_info: {
|
||||||
|
member_href: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigurationQuestion {
|
||||||
|
answerId: number;
|
||||||
|
questionId: number;
|
||||||
|
question: string;
|
||||||
|
|
||||||
|
answer?: string | boolean; // absent on some, boolean for Checkbox in your sample
|
||||||
|
|
||||||
|
sequenceNumber: number;
|
||||||
|
numberOfDecimals: number;
|
||||||
|
|
||||||
|
fieldType: "Text" | "TextArea" | "Password" | "Date" | "Checkbox" | string;
|
||||||
|
requiredFlag: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigurationInfo {
|
||||||
|
lastUpdated: string; // ISO-8601
|
||||||
|
updatedBy: string;
|
||||||
|
dateEntered: string; // ISO-8601
|
||||||
|
enteredBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your payload is an array:
|
||||||
|
export type ConfigurationResponse = ConfigurationItem[];
|
||||||
|
|||||||
Reference in New Issue
Block a user