@kernhq/module-hr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +662 -0
  2. package/README.md +71 -0
  3. package/dist/contract/capabilities.d.ts +45 -0
  4. package/dist/contract/capabilities.d.ts.map +1 -0
  5. package/dist/contract/capabilities.js +114 -0
  6. package/dist/contract/capabilities.js.map +1 -0
  7. package/dist/contract/events.d.ts +74 -0
  8. package/dist/contract/events.d.ts.map +1 -0
  9. package/dist/contract/events.js +63 -0
  10. package/dist/contract/events.js.map +1 -0
  11. package/dist/contract/index.d.ts +15 -0
  12. package/dist/contract/index.d.ts.map +1 -0
  13. package/dist/contract/index.js +15 -0
  14. package/dist/contract/index.js.map +1 -0
  15. package/dist/contract/models.d.ts +457 -0
  16. package/dist/contract/models.d.ts.map +1 -0
  17. package/dist/contract/models.js +381 -0
  18. package/dist/contract/models.js.map +1 -0
  19. package/dist/contract/permissions.d.ts +174 -0
  20. package/dist/contract/permissions.d.ts.map +1 -0
  21. package/dist/contract/permissions.js +209 -0
  22. package/dist/contract/permissions.js.map +1 -0
  23. package/dist/contract/router.d.ts +2720 -0
  24. package/dist/contract/router.d.ts.map +1 -0
  25. package/dist/contract/router.js +520 -0
  26. package/dist/contract/router.js.map +1 -0
  27. package/dist/contract/settings.d.ts +20 -0
  28. package/dist/contract/settings.d.ts.map +1 -0
  29. package/dist/contract/settings.js +34 -0
  30. package/dist/contract/settings.js.map +1 -0
  31. package/dist/policy/calendar.d.ts +64 -0
  32. package/dist/policy/calendar.d.ts.map +1 -0
  33. package/dist/policy/calendar.js +113 -0
  34. package/dist/policy/calendar.js.map +1 -0
  35. package/dist/server/index.d.ts +8 -0
  36. package/dist/server/index.d.ts.map +1 -0
  37. package/dist/server/index.js +141 -0
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/packs/index.d.ts +50 -0
  40. package/dist/server/packs/index.d.ts.map +1 -0
  41. package/dist/server/packs/index.js +121 -0
  42. package/dist/server/packs/index.js.map +1 -0
  43. package/dist/server/router.d.ts +3095 -0
  44. package/dist/server/router.d.ts.map +1 -0
  45. package/dist/server/router.js +1537 -0
  46. package/dist/server/router.js.map +1 -0
  47. package/dist/server/schema.d.ts +2828 -0
  48. package/dist/server/schema.d.ts.map +1 -0
  49. package/dist/server/schema.js +313 -0
  50. package/dist/server/schema.js.map +1 -0
  51. package/dist/server/services/db.d.ts +22 -0
  52. package/dist/server/services/db.d.ts.map +1 -0
  53. package/dist/server/services/db.js +23 -0
  54. package/dist/server/services/db.js.map +1 -0
  55. package/dist/server/services/people.d.ts +110 -0
  56. package/dist/server/services/people.d.ts.map +1 -0
  57. package/dist/server/services/people.js +182 -0
  58. package/dist/server/services/people.js.map +1 -0
  59. package/dist/server/services/resolve.d.ts +66 -0
  60. package/dist/server/services/resolve.d.ts.map +1 -0
  61. package/dist/server/services/resolve.js +145 -0
  62. package/dist/server/services/resolve.js.map +1 -0
  63. package/migrations/0000_init.sql +223 -0
  64. package/migrations/0001_rls.sql +141 -0
  65. package/migrations/meta/0000_snapshot.json +1715 -0
  66. package/migrations/meta/_journal.json +20 -0
  67. package/package.json +59 -0
  68. package/src/client/index.ts +62 -0
  69. package/src/contract/capabilities.ts +117 -0
  70. package/src/contract/events.ts +84 -0
  71. package/src/contract/index.ts +14 -0
  72. package/src/contract/models.ts +437 -0
  73. package/src/contract/permissions.ts +217 -0
  74. package/src/contract/router.ts +611 -0
  75. package/src/contract/settings.ts +35 -0
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1787567806015,
9
+ "tag": "0000_init",
10
+ "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "7",
15
+ "when": 1787567807015,
16
+ "tag": "0001_rls",
17
+ "breakpoints": true
18
+ }
19
+ ]
20
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@kernhq/module-hr",
3
+ "version": "0.1.0",
4
+ "description": "Kern HR module: people, offices, org chart, calendars",
5
+ "license": "AGPL-3.0-only",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "migrations",
10
+ "src/client",
11
+ "src/contract"
12
+ ],
13
+ "exports": {
14
+ "./contract": {
15
+ "types": "./dist/contract/index.d.ts",
16
+ "import": "./dist/contract/index.js",
17
+ "default": "./dist/contract/index.js"
18
+ },
19
+ "./server": {
20
+ "types": "./dist/server/index.d.ts",
21
+ "import": "./dist/server/index.js",
22
+ "default": "./dist/server/index.js"
23
+ },
24
+ "./client": {
25
+ "types": "./src/client/index.ts",
26
+ "svelte": "./src/client/index.ts",
27
+ "default": "./src/client/index.ts"
28
+ },
29
+ "./migrations": "./migrations",
30
+ "./client/*": {
31
+ "types": "./src/client/*",
32
+ "svelte": "./src/client/*",
33
+ "default": "./src/client/*"
34
+ }
35
+ },
36
+ "dependencies": {
37
+ "@kernhq/contracts": "^0.5.0",
38
+ "@kernhq/kernel": "^0.6.0",
39
+ "@kernhq/sdk": "^0.1.0",
40
+ "@orpc/contract": "^1.15.0",
41
+ "@orpc/server": "^1.15.0",
42
+ "drizzle-orm": "^0.45.0",
43
+ "zod": "^4.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "@kernhq/tsconfig": "^0.1.0",
47
+ "@types/node": "^24.0.0",
48
+ "drizzle-kit": "^0.31.0",
49
+ "typescript": "~5.9.3",
50
+ "vitest": "^4.0.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc -p tsconfig.json",
54
+ "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
55
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
56
+ "test": "vitest run",
57
+ "db:generate": "drizzle-kit generate"
58
+ }
59
+ }
@@ -0,0 +1,62 @@
1
+ import { createModuleClient, type KernClientOptions } from '@kernhq/sdk'
2
+ import type { ContractRouterClient } from '@orpc/contract'
3
+ import type { HrContract } from '../contract/index.js'
4
+
5
+ /**
6
+ * The client half.
7
+ *
8
+ * Published as **source**, not compiled: the consumer builds the TypeScript and Svelte with its own
9
+ * toolchain, which is what lets `$state` in a module store stay reactive inside the app. Two
10
+ * consequences worth knowing before you edit anything here — nothing in this package compiles it,
11
+ * so `pnpm build` passes over a syntax error and only the app finds it; and `files` in package.json
12
+ * must cover every directory this entry reaches, contract source included.
13
+ *
14
+ * What lives here: the typed API client, and any logic that is about this module but not about a
15
+ * screen (formatting, grouping, parsing). What does not: the `defineClientModule` manifest and the
16
+ * Svelte components, which live in the app so their labels can go through its message catalogue.
17
+ * `pnpm new-module` generates both halves.
18
+ */
19
+ export type HrApi = ContractRouterClient<HrContract>
20
+
21
+ export function createHrClient(opts: KernClientOptions): HrApi {
22
+ return createModuleClient<HrApi>(opts, 'hr')
23
+ }
24
+
25
+ export {
26
+ type Calendar,
27
+ type CalendarDay,
28
+ type CostCenter,
29
+ type CustomFieldDef,
30
+ type Employment,
31
+ HR_PERMISSIONS,
32
+ hrCapabilities,
33
+ hrPermissions,
34
+ type IsoDate,
35
+ type LegalEntity,
36
+ MODULE_ID,
37
+ type Office,
38
+ type OfficeAssignment,
39
+ type OrgUnit,
40
+ type Person,
41
+ type PersonDocument,
42
+ type PersonResolution,
43
+ type PersonStatus,
44
+ type Position,
45
+ type ResolvedCalendarDay,
46
+ type WorkingWeek,
47
+ } from '../contract/index.js'
48
+
49
+ /**
50
+ * The capability ids, so a client contribution gates on a constant rather than a retyped string.
51
+ *
52
+ * Named unqualified — `capability: HR_CAPABILITIES.offices` gives `'offices'`, not `'hr.offices'` —
53
+ * because from inside a module there is only one namespace. The shell adds this module's id when it
54
+ * builds the workspace's set, which is where several modules' capabilities meet.
55
+ */
56
+ export const HR_CAPABILITIES = {
57
+ core: 'core',
58
+ offices: 'offices',
59
+ legalEntities: 'legal_entities',
60
+ calendars: 'calendars',
61
+ documents: 'documents',
62
+ } as const
@@ -0,0 +1,117 @@
1
+ import { defineCapabilities } from '@kernhq/contracts'
2
+
3
+ /**
4
+ * How much HR this workspace has.
5
+ *
6
+ * HR is the module capabilities were built for. One company wants a staff directory and nothing
7
+ * else; a second wants leave, balances and approvals; a third runs shift rosters and clocks people
8
+ * in at a factory gate. Those are three products under one name, and the alternatives to this
9
+ * registry are a code fork per customer or a navigation rail full of features nobody uses.
10
+ *
11
+ * **A capability is declared here only once something is behind it.** A switch that changes nothing
12
+ * is worse than a missing switch: it teaches an administrator that the switchboard does not mean
13
+ * anything. So this list grows with the module rather than describing where the module is going —
14
+ * `leave`, `attendance`, `overtime`, `rosters`, `periods` and `payroll_export` arrive with the
15
+ * phases that implement them.
16
+ *
17
+ * Two rules that decide whether something belongs here at all:
18
+ *
19
+ * - **Not a permission.** "May Ayşe approve leave" is a permission — true for her, false for someone
20
+ * else, in the same workspace. "Does this company do leave" is a capability: one answer for
21
+ * everyone, the owner included.
22
+ * - **Reversible without a migration.** Switching one off writes a boolean into module settings;
23
+ * the rows stay exactly where they are and switching it back on restores them. Anything that would
24
+ * need data thrown away is not a capability, however much it looks like one.
25
+ */
26
+ export const hrCapabilities = defineCapabilities([
27
+ {
28
+ id: 'core',
29
+ label: 'People',
30
+ description: 'The staff directory, employment records and reporting lines',
31
+ required: true,
32
+ level: 1,
33
+ },
34
+ {
35
+ id: 'offices',
36
+ label: 'Offices',
37
+ description: 'More than one place of work, each with its own country, timezone and holidays',
38
+ dependsOn: ['core'],
39
+ // Off by default, and invisible when off — but the *concept* is never absent. A workspace always
40
+ // has exactly one office, built from its country when HR is enabled, and everybody is assigned
41
+ // to it. Switching this on reveals the list and the assignment control; it does not migrate
42
+ // anything, because the shape was there from the first day. A workspace that only ever has one
43
+ // office never meets the word.
44
+ defaultEnabled: false,
45
+ level: 2,
46
+ },
47
+ {
48
+ id: 'legal_entities',
49
+ label: 'Legal entities',
50
+ description: 'Several employing companies, for a group operating across borders',
51
+ // Depends on offices rather than core: a single-site company has one employer by definition, and
52
+ // the question only becomes real once there is more than one place of work.
53
+ dependsOn: ['offices'],
54
+ defaultEnabled: false,
55
+ level: 3,
56
+ },
57
+ {
58
+ id: 'calendars',
59
+ label: 'Holiday calendars',
60
+ description: 'Public holidays, company closures and the working week',
61
+ dependsOn: ['core'],
62
+ // On by default. Every company has holidays, and a directory that does not know when people are
63
+ // off is answering a question nobody asked.
64
+ defaultEnabled: true,
65
+ level: 1,
66
+ },
67
+ {
68
+ id: 'documents',
69
+ label: 'Employee documents',
70
+ description: 'Contracts, identity documents and certificates against a person',
71
+ dependsOn: ['core'],
72
+ defaultEnabled: false,
73
+ level: 2,
74
+ },
75
+ ])
76
+
77
+ export type HrCapabilityId = (typeof hrCapabilities)[number]['id']
78
+
79
+ /**
80
+ * Which procedures sit behind which capability.
81
+ *
82
+ * Declared as data because a missing `requiresCapability` is invisible: the procedure compiles,
83
+ * every other test passes, and the only symptom is a workspace calling a feature it switched off.
84
+ * `module.test.ts` reads this and fails when a procedure named here is not carrying the middleware.
85
+ *
86
+ * A procedure absent from this map belongs to the module as a whole and is reachable whenever HR is
87
+ * on — which for `core` is always, because it is `required`.
88
+ */
89
+ export const hrCapabilityProcedures: Record<string, readonly string[]> = {
90
+ offices: [
91
+ 'offices.list',
92
+ 'offices.get',
93
+ 'offices.create',
94
+ 'offices.update',
95
+ 'offices.archive',
96
+ 'offices.setDefault',
97
+ 'offices.assign',
98
+ 'offices.unassign',
99
+ 'offices.people',
100
+ ],
101
+ legal_entities: ['entities.list', 'entities.get', 'entities.create', 'entities.update', 'entities.archive'],
102
+ calendars: [
103
+ 'calendars.list',
104
+ 'calendars.get',
105
+ 'calendars.create',
106
+ 'calendars.update',
107
+ 'calendars.archive',
108
+ 'calendars.days.list',
109
+ 'calendars.days.add',
110
+ 'calendars.days.update',
111
+ 'calendars.days.remove',
112
+ 'calendars.pack.preview',
113
+ 'calendars.pack.apply',
114
+ 'calendars.workingDays',
115
+ ],
116
+ documents: ['documents.list', 'documents.attach', 'documents.remove'],
117
+ }
@@ -0,0 +1,84 @@
1
+ import { defineEvent, WorkspaceId } from '@kernhq/contracts'
2
+ import { z } from 'zod'
3
+
4
+ /**
5
+ * `hr.<entity>.<action>`. Anything that emits one declares it here.
6
+ *
7
+ * These are what the rest of the product reacts to. Chat wants to know when somebody joins so it can
8
+ * add them to a channel; a future calendar wants office holidays; payroll wants an employment
9
+ * change. The payloads carry ids rather than rows on purpose — a subscriber that needs the record
10
+ * asks for it with its own principal, so an event cannot become a way to read data past a permission
11
+ * check.
12
+ */
13
+ export const hrEvents = {
14
+ personCreated: defineEvent(
15
+ 'hr.person.created',
16
+ z.object({ personId: z.uuid(), workspaceId: WorkspaceId, userId: z.uuid().nullable() }),
17
+ ),
18
+ personUpdated: defineEvent(
19
+ 'hr.person.updated',
20
+ z.object({ personId: z.uuid(), workspaceId: WorkspaceId, fields: z.array(z.string()) }),
21
+ ),
22
+ /**
23
+ * Status moved — onboarding to active, active to terminated.
24
+ *
25
+ * Separate from `personUpdated` because the things that care about a lifecycle change (revoking
26
+ * access, closing a leave balance, ending a payroll line) do not want to filter every profile
27
+ * edit to find it.
28
+ */
29
+ personStatusChanged: defineEvent(
30
+ 'hr.person.status_changed',
31
+ z.object({
32
+ personId: z.uuid(),
33
+ workspaceId: WorkspaceId,
34
+ from: z.string(),
35
+ to: z.string(),
36
+ on: z.iso.date(),
37
+ }),
38
+ ),
39
+ employmentChanged: defineEvent(
40
+ 'hr.employment.changed',
41
+ z.object({
42
+ personId: z.uuid(),
43
+ workspaceId: WorkspaceId,
44
+ employmentId: z.uuid(),
45
+ effectiveFrom: z.iso.date(),
46
+ }),
47
+ ),
48
+ /**
49
+ * Somebody's office changed, or their primary did.
50
+ *
51
+ * Worth its own event because the primary office decides holidays, timezone and policy: anything
52
+ * holding a derived answer for this person has to recompute, and this is how it finds out.
53
+ */
54
+ officeAssignmentChanged: defineEvent(
55
+ 'hr.office_assignment.changed',
56
+ z.object({
57
+ personId: z.uuid(),
58
+ workspaceId: WorkspaceId,
59
+ officeId: z.uuid(),
60
+ isPrimary: z.boolean(),
61
+ effectiveFrom: z.iso.date(),
62
+ }),
63
+ ),
64
+ officeCreated: defineEvent(
65
+ 'hr.office.created',
66
+ z.object({ officeId: z.uuid(), workspaceId: WorkspaceId, country: z.string() }),
67
+ ),
68
+ /**
69
+ * A calendar's days changed — a holiday added, a pack applied.
70
+ *
71
+ * Everything derived from a calendar (working days, leave day counts, later the attendance day
72
+ * sheet) is stale from here. The payload names the date range touched so a consumer can recompute
73
+ * that window rather than everything.
74
+ */
75
+ calendarChanged: defineEvent(
76
+ 'hr.calendar.changed',
77
+ z.object({
78
+ calendarId: z.uuid(),
79
+ workspaceId: WorkspaceId,
80
+ from: z.iso.date().nullable(),
81
+ to: z.iso.date().nullable(),
82
+ }),
83
+ ),
84
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * What HR offers, as data.
3
+ *
4
+ * Imported by **both** halves — the server implements it, the client calls it — so nothing here may
5
+ * touch Node. The contract is the only thing that crosses that line, which is why a procedure that
6
+ * exists here and not in the router is a lie that compiles. `module.test.ts` checks exactly that,
7
+ * and also that every procedure listed in `hrCapabilityProcedures` carries its capability guard.
8
+ */
9
+ export * from './capabilities.js'
10
+ export * from './events.js'
11
+ export * from './models.js'
12
+ export * from './permissions.js'
13
+ export * from './router.js'
14
+ export * from './settings.js'