Extract groombook/api from monorepo with CI workflow
- Add source code from apps/api - Add packages/db and packages/types workspace dependencies - Add GitHub Actions CI workflow (lint, typecheck, test, docker) - Generate pnpm-lock.yaml - Add .gitignore Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Test factories — build typed in-memory entities for unit tests.
|
||||
*
|
||||
* Each factory returns a fully-populated object with valid defaults.
|
||||
* Pass an overrides object to customise specific fields.
|
||||
*
|
||||
* IDs are generated with a deterministic counter so tests produce stable,
|
||||
* readable values (e.g. "staff-1", "client-2") without needing crypto.
|
||||
*
|
||||
* Usage:
|
||||
* import { buildStaff, buildClient, buildPet } from "@groombook/db/factories";
|
||||
*
|
||||
* const manager = buildStaff({ role: "manager" });
|
||||
* const client = buildClient({ name: "Alice Smith" });
|
||||
* const pet = buildPet({ clientId: client.id });
|
||||
*/
|
||||
|
||||
import type { staff, clients, pets, services, appointments } from "./schema.js";
|
||||
|
||||
// ── Counter-based ID factory ─────────────────────────────────────────────────
|
||||
|
||||
const counters: Record<string, number> = {};
|
||||
|
||||
function nextId(prefix: string): string {
|
||||
counters[prefix] = (counters[prefix] ?? 0) + 1;
|
||||
return `${prefix}-${counters[prefix]}`;
|
||||
}
|
||||
|
||||
/** Reset all counters. Call in beforeEach() to keep tests independent. */
|
||||
export function resetFactoryCounters(): void {
|
||||
for (const key of Object.keys(counters)) {
|
||||
delete counters[key];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Type aliases ─────────────────────────────────────────────────────────────
|
||||
|
||||
export type StaffRow = typeof staff.$inferSelect;
|
||||
export type ClientRow = typeof clients.$inferSelect;
|
||||
export type PetRow = typeof pets.$inferSelect;
|
||||
export type ServiceRow = typeof services.$inferSelect;
|
||||
export type AppointmentRow = typeof appointments.$inferSelect;
|
||||
|
||||
// ── Factories ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function buildStaff(overrides: Partial<StaffRow> = {}): StaffRow {
|
||||
const id = nextId("staff");
|
||||
return {
|
||||
id,
|
||||
name: `Staff Member ${id}`,
|
||||
email: `${id}@groombook.test`,
|
||||
oidcSub: `oidc-${id}`,
|
||||
userId: null,
|
||||
role: "groomer",
|
||||
isSuperUser: false,
|
||||
active: true,
|
||||
icalToken: null,
|
||||
createdAt: new Date("2025-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2025-01-01T00:00:00Z"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildClient(overrides: Partial<ClientRow> = {}): ClientRow {
|
||||
const id = nextId("client");
|
||||
return {
|
||||
id,
|
||||
name: `Client ${id}`,
|
||||
email: `${id}@example.com`,
|
||||
phone: "555-0100",
|
||||
address: "1 Main St, Springfield, CA 90000",
|
||||
notes: null,
|
||||
emailOptOut: false,
|
||||
smsOptIn: false,
|
||||
smsConsentDate: null,
|
||||
smsOptOutDate: null,
|
||||
smsConsentText: null,
|
||||
stripeCustomerId: null,
|
||||
status: "active",
|
||||
disabledAt: null,
|
||||
createdAt: new Date("2025-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2025-01-01T00:00:00Z"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPet(overrides: Partial<PetRow> & { clientId: string }): PetRow {
|
||||
const id = nextId("pet");
|
||||
const defaults: PetRow = {
|
||||
id,
|
||||
clientId: overrides.clientId,
|
||||
name: `Pet ${id}`,
|
||||
species: "Dog",
|
||||
breed: "Mixed Breed",
|
||||
weightKg: "15.00",
|
||||
dateOfBirth: new Date("2020-06-15T00:00:00Z"),
|
||||
healthAlerts: null,
|
||||
groomingNotes: null,
|
||||
cutStyle: null,
|
||||
shampooPreference: null,
|
||||
specialCareNotes: null,
|
||||
customFields: {},
|
||||
photoKey: null,
|
||||
photoUploadedAt: null,
|
||||
image: null,
|
||||
createdAt: new Date("2025-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2025-01-01T00:00:00Z"),
|
||||
};
|
||||
return { ...defaults, ...overrides };
|
||||
}
|
||||
|
||||
export function buildService(overrides: Partial<ServiceRow> = {}): ServiceRow {
|
||||
const id = nextId("service");
|
||||
return {
|
||||
id,
|
||||
name: `Service ${id}`,
|
||||
description: "A grooming service",
|
||||
basePriceCents: 6500,
|
||||
durationMinutes: 60,
|
||||
active: true,
|
||||
createdAt: new Date("2025-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2025-01-01T00:00:00Z"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAppointment(
|
||||
overrides: Partial<AppointmentRow> & { clientId: string; petId: string; serviceId: string; staffId: string }
|
||||
): AppointmentRow {
|
||||
const id = nextId("appointment");
|
||||
const startTime = new Date("2025-06-01T10:00:00Z");
|
||||
const endTime = new Date("2025-06-01T11:00:00Z");
|
||||
const defaults: AppointmentRow = {
|
||||
id,
|
||||
clientId: overrides.clientId,
|
||||
petId: overrides.petId,
|
||||
serviceId: overrides.serviceId,
|
||||
staffId: overrides.staffId,
|
||||
batherStaffId: null,
|
||||
seriesId: null,
|
||||
seriesIndex: null,
|
||||
groupId: null,
|
||||
status: "scheduled",
|
||||
startTime,
|
||||
endTime,
|
||||
notes: null,
|
||||
priceCents: null,
|
||||
confirmationStatus: "pending",
|
||||
confirmedAt: null,
|
||||
cancelledAt: null,
|
||||
confirmationToken: null,
|
||||
customerNotes: null,
|
||||
createdAt: new Date("2025-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2025-01-01T00:00:00Z"),
|
||||
};
|
||||
return { ...defaults, ...overrides };
|
||||
}
|
||||
Reference in New Issue
Block a user