@kernhq/module-hr 0.18.1 → 0.20.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.
- package/dist/contract/capabilities.d.ts.map +1 -1
- package/dist/contract/capabilities.js +45 -0
- package/dist/contract/capabilities.js.map +1 -1
- package/dist/contract/exports.d.ts +459 -0
- package/dist/contract/exports.d.ts.map +1 -0
- package/dist/contract/exports.js +331 -0
- package/dist/contract/exports.js.map +1 -0
- package/dist/contract/index.d.ts +2 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +2 -0
- package/dist/contract/index.js.map +1 -1
- package/dist/contract/permissions.d.ts +10 -2
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +44 -5
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/rosters.d.ts +183 -0
- package/dist/contract/rosters.d.ts.map +1 -0
- package/dist/contract/rosters.js +138 -0
- package/dist/contract/rosters.js.map +1 -0
- package/dist/contract/router.d.ts +893 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +220 -1
- package/dist/contract/router.js.map +1 -1
- package/dist/server/router.d.ts +1210 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +578 -1
- package/dist/server/router.js.map +1 -1
- package/dist/server/schema.d.ts +727 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +105 -0
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/exports.d.ts +321 -0
- package/dist/server/services/exports.d.ts.map +1 -0
- package/dist/server/services/exports.js +765 -0
- package/dist/server/services/exports.js.map +1 -0
- package/dist/server/services/rosters.d.ts +166 -0
- package/dist/server/services/rosters.d.ts.map +1 -0
- package/dist/server/services/rosters.js +268 -0
- package/dist/server/services/rosters.js.map +1 -0
- package/migrations/0012_rosters.sql +163 -0
- package/migrations/meta/0012_snapshot.json +4847 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/client/messages.ts +13 -0
- package/src/contract/capabilities.ts +45 -0
- package/src/contract/exports.ts +354 -0
- package/src/contract/index.ts +2 -0
- package/src/contract/permissions.ts +47 -5
- package/src/contract/rosters.ts +156 -0
- package/src/contract/router.ts +256 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
-- Rosters: which shift a person works on a **date**.
|
|
2
|
+
--
|
|
3
|
+
-- `schedules` is a week that repeats for ever, keyed by weekday name. That is the right shape for an
|
|
4
|
+
-- office and cannot express a factory: 4-on-4-off has no weekly period at all, so there is no
|
|
5
|
+
-- `ScheduleWeek` of any length that describes one. These four tables answer the calendar instead.
|
|
6
|
+
--
|
|
7
|
+
-- * `roster_shifts` — Early, Late, Night. Named rather than inlined into every rotation because
|
|
8
|
+
-- coverage groups by it: "Early" in two rotations has to be one column of one grid.
|
|
9
|
+
-- * `roster_patterns` — a rotation, as a cycle of days and the date `days[0]` falls on. `days` is an
|
|
10
|
+
-- array of arrays of shift ids; an empty entry is a planned rest day and two entries is a split
|
|
11
|
+
-- shift. The cycle length is `jsonb_array_length(days)` and is deliberately not a second column.
|
|
12
|
+
-- * `roster_assignments` — a person on a rotation, effective-dated, with a `cycle_offset` that puts
|
|
13
|
+
-- two crews on one rotation out of phase.
|
|
14
|
+
-- * `roster_overrides` — one day that differs, and nothing else. The whole reason a roster is not a
|
|
15
|
+
-- schedule: a schedule change rewrites every day after it, and somebody covering one Tuesday needs
|
|
16
|
+
-- one Tuesday changed.
|
|
17
|
+
--
|
|
18
|
+
-- **Nothing is expanded into rows.** What somebody works on a date is arithmetic from the anchor,
|
|
19
|
+
-- the offset and the cycle. Generating a year of shifts per person is what makes a roster impossible
|
|
20
|
+
-- to change afterwards: moving a crew forward one day becomes a bulk rewrite of thousands of rows
|
|
21
|
+
-- with no way left to tell which of them a human had already corrected.
|
|
22
|
+
--
|
|
23
|
+
-- Additive throughout — four new tables and no change to anything that exists — so the image before
|
|
24
|
+
-- this one reads the schema unchanged and a rollback needs no dump.
|
|
25
|
+
--
|
|
26
|
+
-- Generated by drizzle-kit from `src/server/schema.ts` and then given `if not exists` on every
|
|
27
|
+
-- statement, plus the two things drizzle cannot express: the exclusion constraint and the RLS
|
|
28
|
+
-- blocks. Drizzle keys applied migrations by content hash, so editing any file in this folder
|
|
29
|
+
-- replays every file in it — and a module migration that throws takes down every module in the host
|
|
30
|
+
-- service at boot, not only this one.
|
|
31
|
+
CREATE TABLE IF NOT EXISTS "mod_hr"."roster_assignments" (
|
|
32
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
33
|
+
"workspace_id" uuid NOT NULL,
|
|
34
|
+
"person_id" uuid NOT NULL,
|
|
35
|
+
"pattern_id" uuid NOT NULL,
|
|
36
|
+
"effective_from" date NOT NULL,
|
|
37
|
+
"effective_to" date,
|
|
38
|
+
"cycle_offset" integer DEFAULT 0 NOT NULL,
|
|
39
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
40
|
+
);
|
|
41
|
+
--> statement-breakpoint
|
|
42
|
+
CREATE TABLE IF NOT EXISTS "mod_hr"."roster_overrides" (
|
|
43
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
44
|
+
"workspace_id" uuid NOT NULL,
|
|
45
|
+
"person_id" uuid NOT NULL,
|
|
46
|
+
"business_date" date NOT NULL,
|
|
47
|
+
"shift_ids" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
48
|
+
"note" text,
|
|
49
|
+
"created_by" uuid,
|
|
50
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
51
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
52
|
+
);
|
|
53
|
+
--> statement-breakpoint
|
|
54
|
+
CREATE TABLE IF NOT EXISTS "mod_hr"."roster_patterns" (
|
|
55
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
56
|
+
"workspace_id" uuid NOT NULL,
|
|
57
|
+
"name" text NOT NULL,
|
|
58
|
+
"anchor_date" date NOT NULL,
|
|
59
|
+
"days" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
60
|
+
"archived_at" timestamp with time zone,
|
|
61
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
62
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
63
|
+
);
|
|
64
|
+
--> statement-breakpoint
|
|
65
|
+
CREATE TABLE IF NOT EXISTS "mod_hr"."roster_shifts" (
|
|
66
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
67
|
+
"workspace_id" uuid NOT NULL,
|
|
68
|
+
"name" text NOT NULL,
|
|
69
|
+
"code" text,
|
|
70
|
+
"start_time" text NOT NULL,
|
|
71
|
+
"end_time" text NOT NULL,
|
|
72
|
+
"break_minutes" integer DEFAULT 0 NOT NULL,
|
|
73
|
+
"grace_in_minutes" integer DEFAULT 0 NOT NULL,
|
|
74
|
+
"grace_out_minutes" integer DEFAULT 0 NOT NULL,
|
|
75
|
+
"color" text,
|
|
76
|
+
"archived_at" timestamp with time zone,
|
|
77
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
78
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
79
|
+
);
|
|
80
|
+
--> statement-breakpoint
|
|
81
|
+
CREATE INDEX IF NOT EXISTS "hr_roster_assign_idx" ON "mod_hr"."roster_assignments" USING btree ("workspace_id","person_id","effective_from");--> statement-breakpoint
|
|
82
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "hr_roster_override_uq" ON "mod_hr"."roster_overrides" USING btree ("workspace_id","person_id","business_date");--> statement-breakpoint
|
|
83
|
+
CREATE INDEX IF NOT EXISTS "hr_roster_patterns_ws_idx" ON "mod_hr"."roster_patterns" USING btree ("workspace_id","archived_at");--> statement-breakpoint
|
|
84
|
+
CREATE INDEX IF NOT EXISTS "hr_roster_shifts_ws_idx" ON "mod_hr"."roster_shifts" USING btree ("workspace_id","archived_at");--> statement-breakpoint
|
|
85
|
+
|
|
86
|
+
-- ---------------------------------------------------------------------------------------------
|
|
87
|
+
-- Two rotations may not be in force for one person on the same day.
|
|
88
|
+
--
|
|
89
|
+
-- The same constraint `schedule_assignments` waited five migrations for, and here from the day the
|
|
90
|
+
-- table exists, because the failure it prevents is not theoretical: the resolver picks the
|
|
91
|
+
-- assignment in force with `limit 1`, and with two rows in force it takes whichever the executor
|
|
92
|
+
-- hands back first. On `schedule_assignments` that showed up as `scheduled_minutes` changing
|
|
93
|
+
-- between one recomputation and the next on rows a locked payroll period had already been filed
|
|
94
|
+
-- against — see 0006, which had to repair the data before it could add the constraint. There is
|
|
95
|
+
-- nothing to repair here.
|
|
96
|
+
--
|
|
97
|
+
-- The `where` clause carries the same exception 0006 does: `daterange(from, to, '[]')` raises for a
|
|
98
|
+
-- reversed pair rather than returning an empty range, so without it a backdated assignment would
|
|
99
|
+
-- fail with a raw Postgres range error. Such a row is in force on no day at all — `inForceOn` needs
|
|
100
|
+
-- `effective_from <= d` and `effective_to >= d` at once — so it cannot be the second rotation this
|
|
101
|
+
-- constraint exists to prevent.
|
|
102
|
+
--
|
|
103
|
+
-- `add constraint` has no `if not exists`, hence the catalogue check. `btree_gist` is created in
|
|
104
|
+
-- 0000, which is what lets a uuid sit beside a range under gist; a module reaching for a gist
|
|
105
|
+
-- exclusion constraint without it fails on any clean database with "data type uuid has no default
|
|
106
|
+
-- operator class", during the module's own migration, so the service does not start.
|
|
107
|
+
do $$
|
|
108
|
+
begin
|
|
109
|
+
if not exists (
|
|
110
|
+
select 1
|
|
111
|
+
from pg_constraint c
|
|
112
|
+
join pg_class t on t.oid = c.conrelid
|
|
113
|
+
join pg_namespace n on n.oid = t.relnamespace
|
|
114
|
+
where n.nspname = 'mod_hr'
|
|
115
|
+
and t.relname = 'roster_assignments'
|
|
116
|
+
and c.conname = 'hr_roster_assign_no_overlap'
|
|
117
|
+
) then
|
|
118
|
+
alter table "mod_hr"."roster_assignments"
|
|
119
|
+
add constraint "hr_roster_assign_no_overlap"
|
|
120
|
+
exclude using gist (
|
|
121
|
+
"workspace_id" with =,
|
|
122
|
+
"person_id" with =,
|
|
123
|
+
daterange("effective_from", "effective_to", '[]') with &&
|
|
124
|
+
) where ("effective_to" is null or "effective_to" >= "effective_from");
|
|
125
|
+
end if;
|
|
126
|
+
end $$;--> statement-breakpoint
|
|
127
|
+
|
|
128
|
+
-- ---------------------------------------------------------------------------------------------
|
|
129
|
+
-- Row-level security for the four new tenant tables, in the same shape as 0001 and for the same
|
|
130
|
+
-- reason: the API checks membership and permission first, and this is what is left when a query
|
|
131
|
+
-- reaches the database another way. `create policy` has no `if not exists` at all, so each one is
|
|
132
|
+
-- preceded by a `drop policy if exists` — without that pair, replaying this file throws.
|
|
133
|
+
alter table "mod_hr"."roster_shifts" enable row level security;--> statement-breakpoint
|
|
134
|
+
alter table "mod_hr"."roster_shifts" force row level security;--> statement-breakpoint
|
|
135
|
+
drop policy if exists "roster_shifts_ws_isolation" on "mod_hr"."roster_shifts";
|
|
136
|
+
--> statement-breakpoint
|
|
137
|
+
create policy "roster_shifts_ws_isolation" on "mod_hr"."roster_shifts"
|
|
138
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
139
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
140
|
+
|
|
141
|
+
alter table "mod_hr"."roster_patterns" enable row level security;--> statement-breakpoint
|
|
142
|
+
alter table "mod_hr"."roster_patterns" force row level security;--> statement-breakpoint
|
|
143
|
+
drop policy if exists "roster_patterns_ws_isolation" on "mod_hr"."roster_patterns";
|
|
144
|
+
--> statement-breakpoint
|
|
145
|
+
create policy "roster_patterns_ws_isolation" on "mod_hr"."roster_patterns"
|
|
146
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
147
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
148
|
+
|
|
149
|
+
alter table "mod_hr"."roster_assignments" enable row level security;--> statement-breakpoint
|
|
150
|
+
alter table "mod_hr"."roster_assignments" force row level security;--> statement-breakpoint
|
|
151
|
+
drop policy if exists "roster_assignments_ws_isolation" on "mod_hr"."roster_assignments";
|
|
152
|
+
--> statement-breakpoint
|
|
153
|
+
create policy "roster_assignments_ws_isolation" on "mod_hr"."roster_assignments"
|
|
154
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
155
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
156
|
+
|
|
157
|
+
alter table "mod_hr"."roster_overrides" enable row level security;--> statement-breakpoint
|
|
158
|
+
alter table "mod_hr"."roster_overrides" force row level security;--> statement-breakpoint
|
|
159
|
+
drop policy if exists "roster_overrides_ws_isolation" on "mod_hr"."roster_overrides";
|
|
160
|
+
--> statement-breakpoint
|
|
161
|
+
create policy "roster_overrides_ws_isolation" on "mod_hr"."roster_overrides"
|
|
162
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
163
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));
|