@alexisapp/leave-core
v0.1.3
Published
Platform-agnostic core package for the leave management feature. Contains the GraphQL client, error handling, query/mutation factories, form schemas, domain logic, i18n, and stores.
Maintainers
Keywords
Readme
@leave/core
Platform-agnostic core package for the leave management feature. Contains the GraphQL client, error handling, query/mutation factories, form schemas, domain logic, i18n, and stores.
This is a leaf package — it has zero platform dependencies (react-native, react-dom are
forbidden).
Setup
pnpm install
pnpm build # tsup — produces dist/ with ESM (.mjs) + declarations (.d.ts)
pnpm clean # rm -rf dist
pnpm typecheck # tsc --noEmit
pnpm lint # eslint src/Build
pnpm build runs tsup which produces ESM output (.mjs) and TypeScript declarations (.d.ts) for
all subpath exports into dist/.
Dual-resolution strategy:
- Metro (React Native): resolves the
react-nativecondition → raw.tsinsrc/— Metro transpiles at build time - Vite (Web): resolves the
importcondition → pre-built.mjsindist/
Public API
Client Lifecycle
import { initializeClient, resetClient, execute } from '@leave/core';
import type { ClientConfig, GraphQLEndpoints } from '@leave/core';initializeClient(config: ClientConfig): void
Creates a module-scoped ky instance configured for GraphQL requests. Must be called before any
execute call. Throws if called twice without resetClient() first (singleton guard — see
ADR-011).
resetClient(): void
Resets the module-scoped singleton to null. Call in useEffect cleanup on unmount (prevents stale
closure on MF remount) and in afterEach() in tests.
execute.gateway<TResult, TVariables>({ query, variables, signal? }): Promise<TResult>
Sends a GraphQL request to the gateway service (new leave operations). query is a
TypedDocumentString from generated-gateway/ — TResult and TVariables are inferred from it.
Classifies all errors at this boundary. Auth errors (401/403) trigger onAuthError() callback, then
throw — zero retry.
// Types are inferred from the document — no manual generics needed
const data = await execute.gateway({
query: ListLeaveSelfCertifiedDocument,
variables: { filters: { ... } },
signal,
});execute.hrCore<TResult, TVariables>({ query, variables, signal? }): Promise<TResult>
Same as execute.gateway but targets HR Core service (legacy employee/policy lookups). query is a
TypedDocumentString from generated-hr-core/.
Types
ClientConfig
| Property | Type | Required | Description |
| ------------- | ------------------------------- | -------- | --------------------------------------------------------- |
| endpoints | GraphQLEndpoints | Yes | Fully resolved GraphQL endpoint URLs |
| getToken | () => Promise<string \| null> | No | Mobile only — returns Auth0 JWT |
| onAuthError | () => void | No | Mobile only — called on 401/403 before throwing |
| devToken | string | No | Hardcoded token for dev. Takes precedence over getToken |
GraphQLEndpoints
| Property | Type | Description |
| --------- | -------- | ------------------------------------ |
| gateway | string | Full URL for gateway GraphQL service |
| hrCore | string | Full URL for HR Core GraphQL service |
Error Classes
| Class | Code | Description |
| -------------- | --------------- | --------------------------------------------------- |
| LeaveError | varies | Base class. All domain errors extend this. |
| AuthError | AUTH_ERROR | 401/403. Never retried. Triggers onAuthError(). |
| GraphQLError | GRAPHQL_ERROR | Server returned errors[] in the GraphQL response. |
| NetworkError | NETWORK_ERROR | Fetch failed, timeout, no connectivity. |
| DomainError | DOMAIN_ERROR | Business rule violation from server. |
All errors are classified via classifyError() at the execute boundary — consumers catch typed
LeaveError subclasses. Classification is idempotent (passing a LeaveError through returns it
unchanged). AbortError (host unmounts mid-request) is classified as ABORT_ERROR — a no-op in the
UI (no toast, no error boundary).
Error Code Mapping
ErrorCode enum and getErrorCode(error) provide consumer-facing error classification for UI
messages:
import { ErrorCode, getErrorCode } from '@leave/core';
const code = getErrorCode(error); // e.g. ErrorCode.CONNECTION_FAILUREPlatforms use getErrorCode() to map errors to i18n-keyed display messages. Core provides the enum
and classification; platforms provide the translations.
Error Helpers
import { isRetryable, isHttpError, getErrorMessage } from '@leave/core';isRetryable(error: unknown): boolean—trueforNetworkError,GraphQLError, 5xx HTTP;falseforAuthError,DomainErrorisHttpError(error: unknown): error is HTTPError— type guard for ky'sHTTPErrorgetErrorMessage(error: unknown): { title: string; description: string }— returns i18n-localized error title and description usingErrorCodemapping
AsyncBoundary
import { AsyncBoundary } from '@leave/core/components';Composable boundary combining QueryErrorResetBoundary + ErrorBoundary (react-error-boundary) +
Suspense. Platform packages provide their own fallbackRender.
<AsyncBoundary
suspenseFallback={<LoadingSpinner />}
errorBoundaryProps={{
fallbackRender: ({ error, resetErrorBoundary }) => (
<MyErrorFallback error={error} onRetry={resetErrorBoundary} />
),
}}
>
<MyDataComponent />
</AsyncBoundary>The onReset handler automatically calls QueryErrorResetBoundary.reset() so TanStack Query
retries failed queries on retry.
GraphQL Codegen
Two separate GraphQL services require two codegen configs:
| Service | Endpoint | Codegen Config | Generated Output | Entities |
| ----------- | ------------- | -------------------- | -------------------- | ---------------------------------------------------- |
| Gateway | /v2/graphql | codegen-gateway.ts | generated-gateway/ | LeaveSelfCertified, LeaveChange, SelfCertifiedPolicy |
| HR Core | /graphql | codegen-hr-core.ts | generated-hr-core/ | Leave, Employee, Policy, LeaveBalance |
Commands
pnpm codegen # Run both configs sequentially
pnpm codegen:gateway # Generate gateway types only
pnpm codegen:hr-core # Generate HR Core types onlySchema Source
Schemas are introspected from remote API endpoints at codegen time — no local schema files are stored in the repo. This matches the salary-review-ui pattern.
- Gateway:
https://api-2.dev-alexishr.io/v2/graphql - HR Core:
https://api-2.dev-alexishr.io/graphql
To force a fresh introspection (e.g. after API schema changes): turbo codegen --force
Operation Structure
src/graphql/operations/
├── gateway/ # Operations for execute.gateway()
│ ├── leave-self-certified/ # queries.graphql, mutations.graphql
│ ├── leave-change/ # queries.graphql, mutations.graphql
│ └── self-certified-policy/ # queries.graphql
└── hr-core/ # Operations for execute.hrCore()
├── leave/ # queries.graphql, mutations.graphql
├── policy/ # queries.graphql
└── employee/ # queries.graphqlGenerated Output
generated-gateway/ and generated-hr-core/ — never edit these files manually. They are
overwritten by codegen.
Uses documentMode: 'string' because execute() takes document: string (not AST). All document
constants are TypedDocumentString instances that serialize to string.
Endpoint-to-Domain Mapping
| Operation | Executor | Why |
| ----------------------------------------------- | ----------------- | ------------------------------------- |
| getLeave, listLeave, createLeave, updateLeave | execute.hrCore | Standard leave CRUD in HR Core |
| getEmployeeVacationBalance, listVacationBalance | execute.hrCore | Balance data in HR Core |
| getEmployee | execute.hrCore | Employee data in HR Core |
| getLeavePolicy, listLeavePolicies | execute.hrCore | Leave policies in HR Core |
| getLeaveSelfCertified, create/update | execute.gateway | New self-certified service on gateway |
| getLeaveChange, leaveChangeCreate | execute.gateway | New change-request service on gateway |
| getLeaveSelfCertifiedPolicy | execute.gateway | Self-certified policy on gateway |
Query Factories
Import from @leave/core/queries. Pattern matches salary-review-ui (officesQueries,
employeesQueries).
Four factories with cascading keys and embedded queryOptions (ADR-014):
| Factory | Root key (all) | Domains covered |
| ----------------- | ---------------- | --------------------------------------------------------- |
| leaveQueries | ['leave'] | Leave CRUD, balance, approvals, self-certified, time bank |
| policyQueries | ['policy'] | Leave policies, self-certified policies, leave types |
| employeeQueries | ['employee'] | Employee, employment, work week, holidays |
| settingsQueries | ['settings'] | Time-off settings |
Each factory uses a cascading key hierarchy — every level spreads the parent:
import { leaveQueries } from '@leave/core/queries';
// Key-only levels (for invalidation targeting)
leaveQueries.all // ['leave']
leaveQueries.lists() // [...all, 'list']
leaveQueries.details() // [...all, 'detail']
// Leaf levels accept codegen-typed QueryVariables and pass them directly to execute
leaveQueries.list(variables?) // queryOptions({ queryKey: [...lists(), variables], queryFn })
leaveQueries.detail(variables) // queryOptions({ queryKey: [...details(), variables], queryFn })
// Usage with TanStack Query — consumers build the variables object at the call site
const { data } = useQuery(leaveQueries.list({ employeeIdList: [employeeId] }));
const { data } = useQuery(leaveQueries.detail({ id: leaveId }));
const { data } = useQuery(leaveQueries.balance({ id: employeeId }));
// Nuclear invalidation — wipes all leave queries
queryClient.invalidateQueries({ queryKey: leaveQueries.all });All leaf methods accept codegen-typed QueryVariables and pass them through — never remap or rename
fields inside the factory. All queryFn implementations pass { signal } to execute for request
cancellation on component unmount.
Subpath Exports
| Subpath | Purpose |
| -------------- | ------------------------------------------------------------------------------- |
| @leave/core | Client lifecycle (initializeClient, resetClient, execute) |
| ./queries | TanStack Query queryOptions factories |
| ./mutations | Mutation wrappers (callback-based) |
| ./forms | TanStack Form + Zod form schemas |
| ./domain | Business rules, permissions, types |
| ./i18n | i18next instance + locale resources |
| ./components | AsyncBoundary (react-error-boundary + Suspense + QueryErrorResetBoundary) |
| ./stores | Zustand stores |
| ./hooks | Shared hooks (useCurrentEmployeeId, useLeaveList, useBalances, useBalanceTiles) |
| ./utils | Shared utils (dateUtils, apiUtils, typeSafeUtils) |
| ./constants | Const enums (LeaveDisplayStatus, LeaveChangeType, FILTER_KEYS) |
| ./types | Domain types (ILeaveListItem, ILeaveDisplayStatus, IPagination) |
Metro (React Native) resolves raw .ts via the react-native condition. Bundler/Node resolves
compiled dist/.
Peer Dependencies
| Package | Range |
| ----------------------- | ---------------------- |
| react | ^18.0.0 \|\| ^19.0.0 |
| @tanstack/react-query | ^5.0.0 |
| react-error-boundary | ^5.0.0 |
| zod | ^4.0.0 |
Dependencies
| Package | Range | Purpose |
| --------- | --------- | ---------------------------- |
| ky | ^1.0.0 | HTTP client (timeout, retry) |
| i18next | ^23.0.0 | Internationalization |
| graphql | ^16.0.0 | GraphQL utilities |
