355 lines
15 KiB
JavaScript
355 lines
15 KiB
JavaScript
// index.ts
|
|
import express from "express";
|
|
import cors from "cors";
|
|
import cookieParser from "cookie-parser";
|
|
import * as trpcExpress from "@trpc/server/adapters/express";
|
|
|
|
// trpc.ts
|
|
import { createQueryBuilder } from "@taylordb/query-builder";
|
|
import { initTRPC } from "@trpc/server";
|
|
var createContext = ({ req, res }) => {
|
|
const appAccessToken = req.cookies?.app_access_token;
|
|
if (!appAccessToken) {
|
|
throw new Error("Unauthorized: app_access_token cookie is required");
|
|
}
|
|
const queryBuilder = createQueryBuilder({
|
|
baseUrl: process.env.TAYLORDB_BASE_URL,
|
|
baseId: process.env.TAYLORDB_SERVER_ID,
|
|
apiKey: appAccessToken
|
|
});
|
|
return {
|
|
req,
|
|
res,
|
|
queryBuilder
|
|
};
|
|
};
|
|
var t = initTRPC.context().create();
|
|
var router = t.router;
|
|
var publicProcedure = t.procedure;
|
|
|
|
// routers/companies.ts
|
|
import { z } from "zod";
|
|
var companiesRouter = router({
|
|
getAll: publicProcedure.query(async ({ ctx }) => {
|
|
return await ctx.queryBuilder.selectFrom("companies").select(["id", "name", "website", "notes", "createdAt", "updatedAt"]).orderBy("createdAt", "desc").execute();
|
|
}),
|
|
search: publicProcedure.input(z.object({ query: z.string(), limit: z.number().default(5) })).query(async ({ ctx, input }) => {
|
|
const searchTerm = input.query.trim();
|
|
if (!searchTerm) {
|
|
return await ctx.queryBuilder.selectFrom("companies").select(["id", "name", "website"]).orderBy("createdAt", "desc").limit(input.limit).execute();
|
|
}
|
|
return await ctx.queryBuilder.selectFrom("companies").select(["id", "name", "website"]).where("name", "contains", searchTerm).orWhere("website", "contains", searchTerm).orderBy("createdAt", "desc").limit(input.limit).execute();
|
|
}),
|
|
getById: publicProcedure.input(z.object({ id: z.number() })).query(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.selectFrom("companies").select(["id", "name", "website", "notes", "createdAt", "updatedAt"]).where("id", "=", input.id).executeTakeFirst();
|
|
}),
|
|
create: publicProcedure.input(
|
|
z.object({
|
|
name: z.string().min(1),
|
|
website: z.string().optional(),
|
|
notes: z.string().optional()
|
|
})
|
|
).mutation(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.insertInto("companies").values(input).executeTakeFirst();
|
|
}),
|
|
update: publicProcedure.input(
|
|
z.object({
|
|
id: z.number(),
|
|
name: z.string().optional(),
|
|
website: z.string().optional(),
|
|
notes: z.string().optional()
|
|
})
|
|
).mutation(async ({ ctx, input }) => {
|
|
const { id, ...data } = input;
|
|
return await ctx.queryBuilder.update("companies").set(data).where("id", "=", id).execute();
|
|
}),
|
|
delete: publicProcedure.input(z.object({ id: z.number() })).mutation(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.deleteFrom("companies").where("id", "=", input.id).execute();
|
|
})
|
|
});
|
|
|
|
// routers/contacts.ts
|
|
import { z as z2 } from "zod";
|
|
var contactsRouter = router({
|
|
getAll: publicProcedure.query(async ({ ctx }) => {
|
|
return await ctx.queryBuilder.selectFrom("contacts").select(["id", "firstName", "lastName", "mail", "phone1", "phone2", "notes", "createdAt", "updatedAt"]).orderBy("createdAt", "desc").execute();
|
|
}),
|
|
search: publicProcedure.input(z2.object({ query: z2.string(), limit: z2.number().default(5) })).query(async ({ ctx, input }) => {
|
|
const searchTerm = input.query.trim();
|
|
if (!searchTerm) {
|
|
return await ctx.queryBuilder.selectFrom("contacts").select(["id", "firstName", "lastName", "mail"]).orderBy("createdAt", "desc").limit(input.limit).execute();
|
|
}
|
|
return await ctx.queryBuilder.selectFrom("contacts").select(["id", "firstName", "lastName", "mail"]).where("firstName", "contains", searchTerm).orWhere("lastName", "contains", searchTerm).orWhere("mail", "contains", searchTerm).orderBy("createdAt", "desc").limit(input.limit).execute();
|
|
}),
|
|
getById: publicProcedure.input(z2.object({ id: z2.number() })).query(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.selectFrom("contacts").select(["id", "firstName", "lastName", "mail", "phone1", "phone2", "notes", "createdAt", "updatedAt"]).with({ company: (qb) => qb.select(["id", "name", "website"]) }).where("id", "=", input.id).executeTakeFirst();
|
|
}),
|
|
create: publicProcedure.input(
|
|
z2.object({
|
|
firstName: z2.string().min(1),
|
|
lastName: z2.string().min(1),
|
|
mail: z2.string().email().optional().or(z2.literal("")),
|
|
phone1: z2.string().optional(),
|
|
phone2: z2.string().optional(),
|
|
notes: z2.string().optional(),
|
|
company: z2.array(z2.number()).optional()
|
|
})
|
|
).mutation(async ({ ctx, input }) => {
|
|
const values = {
|
|
...input,
|
|
mail: input.mail || void 0
|
|
};
|
|
return await ctx.queryBuilder.insertInto("contacts").values(values).executeTakeFirst();
|
|
}),
|
|
update: publicProcedure.input(
|
|
z2.object({
|
|
id: z2.number(),
|
|
firstName: z2.string().optional(),
|
|
lastName: z2.string().optional(),
|
|
mail: z2.string().email().optional().or(z2.literal("")),
|
|
phone1: z2.string().optional(),
|
|
phone2: z2.string().optional(),
|
|
notes: z2.string().optional(),
|
|
company: z2.array(z2.number()).optional()
|
|
})
|
|
).mutation(async ({ ctx, input }) => {
|
|
const { id, ...data } = input;
|
|
const updateData = Object.fromEntries(
|
|
Object.entries(data).filter(([key, v]) => {
|
|
if (v === void 0) return false;
|
|
if (key === "mail" && v === "") return false;
|
|
return true;
|
|
})
|
|
);
|
|
return await ctx.queryBuilder.update("contacts").set(updateData).where("id", "=", id).execute();
|
|
}),
|
|
delete: publicProcedure.input(z2.object({ id: z2.number() })).mutation(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.deleteFrom("contacts").where("id", "=", input.id).execute();
|
|
})
|
|
});
|
|
|
|
// routers/deals.ts
|
|
import { z as z3 } from "zod";
|
|
var DealsPhaseOptions = [
|
|
"New",
|
|
"Demo scheduled",
|
|
"Contract signed",
|
|
"Lost"
|
|
];
|
|
var phaseEnum = z3.enum(DealsPhaseOptions);
|
|
var dealsRouter = router({
|
|
getAll: publicProcedure.query(async ({ ctx }) => {
|
|
return await ctx.queryBuilder.selectFrom("deals").select(["id", "name", "phase", "createdAt", "updatedAt"]).with({
|
|
mainContact: (qb) => qb.select(["id", "firstName", "lastName", "mail"])
|
|
}).orderBy("createdAt", "desc").execute();
|
|
}),
|
|
getById: publicProcedure.input(z3.object({ id: z3.number() })).query(async ({ ctx, input }) => {
|
|
const deal = await ctx.queryBuilder.selectFrom("deals").select(["id", "name", "phase", "createdAt", "updatedAt"]).with({ mainContact: (qb) => qb.select(["id", "firstName", "lastName", "mail"]) }).where("id", "=", input.id).executeTakeFirst();
|
|
return deal;
|
|
}),
|
|
search: publicProcedure.input(z3.object({ query: z3.string(), limit: z3.number().optional() })).query(async ({ ctx, input }) => {
|
|
let query = ctx.queryBuilder.selectFrom("deals").select(["id", "name", "phase"]);
|
|
if (input.query.trim()) {
|
|
query = query.where("name", "contains", input.query);
|
|
}
|
|
return await query.orderBy("name", "asc").execute();
|
|
}),
|
|
// Get detailed deal info with contact and company for details page
|
|
getDetails: publicProcedure.input(z3.object({ id: z3.number() })).query(async ({ ctx, input }) => {
|
|
const deal = await ctx.queryBuilder.selectFrom("deals").select(["id", "name", "phase", "lostReason", "createdAt", "updatedAt"]).with({
|
|
mainContact: (qb) => qb.select(["id", "firstName", "lastName", "mail", "phone1", "phone2", "notes"]).with({ company: (companyQb) => companyQb.select(["id", "name", "website", "notes"]) })
|
|
}).with({
|
|
activities: (qb) => qb.select(["id", "name", "type", "plannedAt", "doneAt", "createdAt"]).with({ notes: (notesQb) => notesQb.select(["id", "name", "createdAt"]) })
|
|
}).where("id", "=", input.id).executeTakeFirst();
|
|
return deal;
|
|
}),
|
|
create: publicProcedure.input(
|
|
z3.object({
|
|
name: z3.string().min(1),
|
|
phase: phaseEnum.default("New"),
|
|
mainContact: z3.array(z3.number()).optional()
|
|
})
|
|
).mutation(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.insertInto("deals").values(input).executeTakeFirst();
|
|
}),
|
|
update: publicProcedure.input(
|
|
z3.object({
|
|
id: z3.number(),
|
|
name: z3.string().optional(),
|
|
phase: phaseEnum.optional(),
|
|
mainContact: z3.array(z3.number()).optional()
|
|
})
|
|
).mutation(async ({ ctx, input }) => {
|
|
const { id, ...data } = input;
|
|
const updateData = Object.fromEntries(
|
|
Object.entries(data).filter(([, v]) => v !== void 0)
|
|
);
|
|
return await ctx.queryBuilder.update("deals").set(updateData).where("id", "=", id).execute();
|
|
}),
|
|
delete: publicProcedure.input(z3.object({ id: z3.number() })).mutation(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.deleteFrom("deals").where("id", "=", input.id).execute();
|
|
})
|
|
});
|
|
|
|
// routers/activities.ts
|
|
import { z as z4 } from "zod";
|
|
var ActivitiesTypeOptions = [
|
|
"Call",
|
|
"Meeting",
|
|
"Task",
|
|
"Demo"
|
|
];
|
|
var activitiesRouter = router({
|
|
getAll: publicProcedure.input(
|
|
z4.object({
|
|
dateFilter: z4.enum(["all", "today", "thisWeek", "thisMonth"]).optional(),
|
|
typeFilter: z4.enum(["all", ...ActivitiesTypeOptions]).optional()
|
|
}).optional()
|
|
).query(async ({ ctx, input }) => {
|
|
let query = ctx.queryBuilder.selectFrom("activities").select(["id", "name", "type", "plannedAt", "doneAt", "createdAt", "updatedAt"]).with({ deal: (qb) => qb.select(["id", "name"]) }).with({ notes: (qb) => qb.select(["id", "name"]) });
|
|
if (input?.dateFilter && input.dateFilter !== "all") {
|
|
if (input.dateFilter === "today") {
|
|
query = query.where("plannedAt", "=", "today");
|
|
} else if (input.dateFilter === "thisWeek") {
|
|
query = query.where("plannedAt", "isWithIn", "currentWeek");
|
|
} else if (input.dateFilter === "thisMonth") {
|
|
query = query.where("plannedAt", "isWithIn", "currentMonth");
|
|
}
|
|
}
|
|
if (input?.typeFilter && input.typeFilter !== "all") {
|
|
query = query.where("type", "=", input.typeFilter);
|
|
}
|
|
return await query.orderBy("plannedAt", "desc").execute();
|
|
}),
|
|
getByDeal: publicProcedure.input(z4.object({ dealId: z4.number() })).query(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.selectFrom("activities").select(["id", "name", "type", "plannedAt", "doneAt", "createdAt", "updatedAt"]).with({ notes: (qb) => qb.select(["id", "name"]) }).where("deal", "=", input.dealId).orderBy("plannedAt", "asc").execute();
|
|
}),
|
|
getById: publicProcedure.input(z4.object({ id: z4.number() })).query(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.selectFrom("activities").select(["id", "name", "type", "plannedAt", "doneAt", "createdAt", "updatedAt"]).with({ deal: (qb) => qb.select(["id", "name"]) }).with({ notes: (qb) => qb.select(["id", "name"]) }).where("id", "=", input.id).executeTakeFirst();
|
|
}),
|
|
create: publicProcedure.input(
|
|
z4.object({
|
|
name: z4.string().min(1),
|
|
deal: z4.array(z4.number()).optional(),
|
|
plannedAt: z4.string().optional(),
|
|
doneAt: z4.string().optional(),
|
|
type: z4.enum(ActivitiesTypeOptions).optional()
|
|
})
|
|
).mutation(async ({ ctx, input }) => {
|
|
const values = {
|
|
name: input.name
|
|
};
|
|
if (input.deal) values.deal = input.deal;
|
|
if (input.plannedAt) values.plannedAt = input.plannedAt;
|
|
if (input.doneAt) values.doneAt = input.doneAt;
|
|
if (input.type) values.type = input.type;
|
|
return await ctx.queryBuilder.insertInto("activities").values(values).executeTakeFirst();
|
|
}),
|
|
update: publicProcedure.input(
|
|
z4.object({
|
|
id: z4.number(),
|
|
name: z4.string().optional(),
|
|
plannedAt: z4.string().optional(),
|
|
doneAt: z4.string().optional(),
|
|
notes: z4.array(z4.number()).optional(),
|
|
type: z4.enum(ActivitiesTypeOptions).optional()
|
|
})
|
|
).mutation(async ({ ctx, input }) => {
|
|
const { id, ...data } = input;
|
|
const updateData = Object.fromEntries(
|
|
Object.entries(data).filter(([, v]) => v !== void 0)
|
|
);
|
|
return await ctx.queryBuilder.update("activities").set(updateData).where("id", "=", id).execute();
|
|
}),
|
|
complete: publicProcedure.input(z4.object({ id: z4.number() })).mutation(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.update("activities").set({ doneAt: (/* @__PURE__ */ new Date()).toISOString() }).where("id", "=", input.id).execute();
|
|
}),
|
|
uncomplete: publicProcedure.input(z4.object({ id: z4.number() })).mutation(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.update("activities").set({ doneAt: null }).where("id", "=", input.id).execute();
|
|
}),
|
|
delete: publicProcedure.input(z4.object({ id: z4.number() })).mutation(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.deleteFrom("activities").where("id", "=", input.id).execute();
|
|
})
|
|
});
|
|
|
|
// routers/notes.ts
|
|
import { z as z5 } from "zod";
|
|
var notesRouter = router({
|
|
getByActivity: publicProcedure.input(z5.object({ activityId: z5.number() })).query(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.selectFrom("notes").select(["id", "name", "createdAt", "updatedAt"]).where("activities1", "=", input.activityId).orderBy("createdAt", "desc").execute();
|
|
}),
|
|
getById: publicProcedure.input(z5.object({ id: z5.number() })).query(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.selectFrom("notes").select(["id", "name", "createdAt", "updatedAt"]).where("id", "=", input.id).executeTakeFirst();
|
|
}),
|
|
create: publicProcedure.input(
|
|
z5.object({
|
|
name: z5.string().min(1),
|
|
activities1: z5.array(z5.number())
|
|
})
|
|
).mutation(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.insertInto("notes").values(input).executeTakeFirst();
|
|
}),
|
|
update: publicProcedure.input(
|
|
z5.object({
|
|
id: z5.number(),
|
|
name: z5.string().optional()
|
|
})
|
|
).mutation(async ({ ctx, input }) => {
|
|
const { id, ...data } = input;
|
|
const updateData = Object.fromEntries(
|
|
Object.entries(data).filter(([, v]) => v !== void 0)
|
|
);
|
|
return await ctx.queryBuilder.update("notes").set(updateData).where("id", "=", id).execute();
|
|
}),
|
|
delete: publicProcedure.input(z5.object({ id: z5.number() })).mutation(async ({ ctx, input }) => {
|
|
return await ctx.queryBuilder.deleteFrom("notes").where("id", "=", input.id).execute();
|
|
})
|
|
});
|
|
|
|
// router.ts
|
|
var appRouter = router({
|
|
companies: companiesRouter,
|
|
contacts: contactsRouter,
|
|
deals: dealsRouter,
|
|
activities: activitiesRouter,
|
|
notes: notesRouter
|
|
});
|
|
|
|
// index.ts
|
|
var app = express();
|
|
var PORT = process.env.PORT || 3001;
|
|
app.use(cookieParser());
|
|
app.use(
|
|
cors({
|
|
origin: [
|
|
process.env.FRONTEND_URL || "http://localhost:5173",
|
|
"http://localhost:5174"
|
|
],
|
|
credentials: true
|
|
})
|
|
);
|
|
app.get("/api/health", (_req, res) => {
|
|
res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
});
|
|
app.use(
|
|
"/api/trpc",
|
|
(req, res, next) => {
|
|
if (req.path === "/" || req.path === "") {
|
|
return res.json({
|
|
message: "TaylorDB tRPC server is running!",
|
|
health: `http://${req.headers.host}/api/trpc/health`,
|
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
});
|
|
}
|
|
next();
|
|
},
|
|
trpcExpress.createExpressMiddleware({
|
|
router: appRouter,
|
|
createContext
|
|
})
|
|
);
|
|
app.listen(PORT, () => {
|
|
console.log(`\u{1F680} Server running on http://localhost:${PORT}`);
|
|
console.log(`\u{1F4E1} tRPC endpoint: http://localhost:${PORT}/api/trpc`);
|
|
});
|