@classytic/people
v0.1.1
Published
Framework-agnostic core HR engine — employees, organization structure, contracts, and effective-dated reporting lines. Powered by MongoDB via @classytic/mongokit.
Readme
@classytic/people
Framework-agnostic core HR engine — Employee, Department, Position, EmploymentContract. The foundation every other @classytic/* HR package (muster, leave, payrun, etc.) consumes via bridge ports.
- MongoKit repositories ARE the domain layer — Arc auto-CRUD works directly.
- Zod v4 schemas exported via
./schemas/*for Arc auto-OpenAPI. - Events use the
people:prefix. - Host owns outbox durability (per PACKAGE_RULES §5.5);
outbox.savefailures propagate (P8). - Domain verbs accept the public slug (
EMP-2026-0001) ANDString(doc._id)(P9 dual-ID). - Domain errors implement repo-core's
HttpError(status+code) — arc serializes them without adapters. - Soft-deleted rows for audit reads: inherited
findAll(filters, { ...repoOptionsFromCtx(ctx), includeDeleted: true }).
Requires @classytic/mongokit >=3.16.0 and @classytic/repo-core >=0.6.0 (fail-closed tenant defaults; signal / retryPolicy honored on every repo op).
Install
npm install @classytic/people @classytic/mongokit @classytic/primitives mongoose zodQuick start
import mongoose from 'mongoose';
import { createPeople } from '@classytic/people';
await mongoose.connect(process.env.MONGO_URI!);
const people = await createPeople({
connection: mongoose.connection,
// repo-core TenantConfig shape (or `false` for single-tenant deployments)
tenant: { fieldType: 'objectId' },
// optional — host-provided bridges
bridges: {
user: myUserBridge,
},
// optional — host-owned durable outbox (PACKAGE_RULES §5.5 + P8):
// outbox.save runs BEFORE transport publish and its failures propagate,
// so a host-driven transaction rolls back instead of losing the event.
// outbox: myOutboxStore,
});
const ctx = {
organizationId: '507f1f77bcf86cd799439011',
actorId: '507f191e810c19729de860ea',
actorKind: 'user' as const,
correlationId: crypto.randomUUID(),
};
const employee = await people.repositories.employee.hire(
{
name: { given: 'Ayesha', family: 'Rahman' },
contact: { email: '[email protected]' },
employmentType: 'full_time',
hireDate: new Date(),
},
ctx,
);Arc integration — zero adapter code
EmployeeRepository extends mongokit.Repository<EmployeeDocument> and sets idField = 'employeeNumber'. Arc's BaseController auto-derives both from RepositoryLike, so defineResource({ adapter: createMongooseAdapter({ model, repository }) }) wires the full CRUD surface — no controller subclass, no proxies.
import { defineResource, createMongooseAdapter } from '@classytic/arc';
import { requireRoles, allowPublic } from '@classytic/arc/permissions';
import {
hireEmployeeSchema,
updateEmployeeSchema,
terminateEmployeeSchema,
rehireEmployeeSchema,
transferDepartmentSchema,
promoteToPositionSchema,
changeEmploymentStatusSchema,
} from '@classytic/people/schemas/employee';
export default defineResource({
name: 'employee',
adapter: createMongooseAdapter({
model: people.models.Employee,
repository: people.repositories.employee,
}),
presets: ['softDelete', 'multiTenant'],
openApiSchemas: {
createBody: hireEmployeeSchema, // zod v4 — arc auto-converts to JSON Schema
updateBody: updateEmployeeSchema,
},
permissions: {
list: requireRoles(['admin', 'hr_manager']),
get: requireRoles(['admin', 'hr_manager', 'employee']),
create: requireRoles(['admin', 'hr_manager']),
update: requireRoles(['admin', 'hr_manager']),
delete: requireRoles(['admin']),
},
actions: {
terminate: {
handler: (id, data, req) =>
people.repositories.employee.terminate(id, data, buildCtx(req)),
schema: terminateEmployeeSchema,
permissions: requireRoles(['admin', 'hr_manager']),
},
rehire: {
handler: (id, data, req) =>
people.repositories.employee.rehire(id, data, buildCtx(req)),
schema: rehireEmployeeSchema,
permissions: requireRoles(['admin', 'hr_manager']),
},
transferDepartment: {
handler: (id, data, req) =>
people.repositories.employee.transferToDepartment(
id,
data.departmentId,
buildCtx(req),
),
schema: transferDepartmentSchema,
},
promote: {
handler: (id, data, req) =>
people.repositories.employee.promoteTo(id, data.positionId, buildCtx(req)),
schema: promoteToPositionSchema,
},
changeStatus: {
handler: (id, data, req) =>
people.repositories.employee.changeEmploymentStatus(
id,
data.toStatus,
buildCtx(req),
),
schema: changeEmploymentStatusSchema,
},
},
});Composition with other @classytic HR packages
| Package | Depends on people via |
|---|---|
| @classytic/muster | EmployeeBridge — resolves subjectId when subject is an employee |
| @classytic/leave | EmployeeBridge |
| @classytic/payrun | EmployeeBridge + EmploymentContractBridge for salary structure |
Each HR package is independently usable — muster can track gym members, leave can track student absences — the bridge contract is optional. When running a full HR suite, wire people's bridge implementations into the others.
