@kernhq/module-hr 0.2.0 → 0.4.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/approvals.d.ts +281 -0
- package/dist/contract/approvals.d.ts.map +1 -0
- package/dist/contract/approvals.js +127 -0
- package/dist/contract/approvals.js.map +1 -0
- package/dist/contract/attendance.d.ts +240 -0
- package/dist/contract/attendance.d.ts.map +1 -0
- package/dist/contract/attendance.js +154 -0
- package/dist/contract/attendance.js.map +1 -0
- package/dist/contract/capabilities.d.ts +1 -1
- package/dist/contract/capabilities.d.ts.map +1 -1
- package/dist/contract/capabilities.js +80 -0
- package/dist/contract/capabilities.js.map +1 -1
- package/dist/contract/events.d.ts +60 -0
- package/dist/contract/events.d.ts.map +1 -1
- package/dist/contract/events.js +60 -0
- package/dist/contract/events.js.map +1 -1
- package/dist/contract/index.d.ts +3 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +3 -0
- package/dist/contract/index.js.map +1 -1
- package/dist/contract/leave.d.ts +168 -0
- package/dist/contract/leave.d.ts.map +1 -0
- package/dist/contract/leave.js +150 -0
- package/dist/contract/leave.js.map +1 -0
- package/dist/contract/permissions.d.ts +100 -0
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +119 -0
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/router.d.ts +2440 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +393 -0
- package/dist/contract/router.js.map +1 -1
- package/dist/policy/time.d.ts +48 -0
- package/dist/policy/time.d.ts.map +1 -0
- package/dist/policy/time.js +144 -0
- package/dist/policy/time.js.map +1 -0
- package/dist/policy/working-time.d.ts +98 -0
- package/dist/policy/working-time.d.ts.map +1 -0
- package/dist/policy/working-time.js +190 -0
- package/dist/policy/working-time.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/jobs.d.ts +16 -0
- package/dist/server/jobs.d.ts.map +1 -0
- package/dist/server/jobs.js +166 -0
- package/dist/server/jobs.js.map +1 -0
- package/dist/server/router.d.ts +2731 -58
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +1306 -3
- package/dist/server/router.js.map +1 -1
- package/dist/server/schema.d.ts +3344 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +325 -0
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/approvals.d.ts +163 -0
- package/dist/server/services/approvals.d.ts.map +1 -0
- package/dist/server/services/approvals.js +325 -0
- package/dist/server/services/approvals.js.map +1 -0
- package/dist/server/services/attendance.d.ts +150 -0
- package/dist/server/services/attendance.d.ts.map +1 -0
- package/dist/server/services/attendance.js +239 -0
- package/dist/server/services/attendance.js.map +1 -0
- package/dist/server/services/ledger.d.ts +135 -0
- package/dist/server/services/ledger.d.ts.map +1 -0
- package/dist/server/services/ledger.js +178 -0
- package/dist/server/services/ledger.js.map +1 -0
- package/dist/server/services/people.d.ts +3 -3
- package/migrations/0002_leave.sql +237 -0
- package/migrations/0003_attendance.sql +206 -0
- package/migrations/meta/0002_snapshot.json +3009 -0
- package/migrations/meta/0003_snapshot.json +3681 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/src/contract/approvals.ts +149 -0
- package/src/contract/attendance.ts +180 -0
- package/src/contract/capabilities.ts +80 -0
- package/src/contract/events.ts +81 -0
- package/src/contract/index.ts +3 -0
- package/src/contract/leave.ts +171 -0
- package/src/contract/permissions.ts +122 -0
- package/src/contract/router.ts +481 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
CREATE TABLE "mod_hr"."approval_chains" (
|
|
2
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
3
|
+
"workspace_id" uuid NOT NULL,
|
|
4
|
+
"name" text NOT NULL,
|
|
5
|
+
"subject_type" text NOT NULL,
|
|
6
|
+
"spec" jsonb NOT NULL,
|
|
7
|
+
"is_default" boolean DEFAULT false NOT NULL,
|
|
8
|
+
"archived_at" timestamp with time zone,
|
|
9
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
10
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
11
|
+
);
|
|
12
|
+
--> statement-breakpoint
|
|
13
|
+
CREATE TABLE "mod_hr"."approval_decisions" (
|
|
14
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
15
|
+
"workspace_id" uuid NOT NULL,
|
|
16
|
+
"step_id" uuid NOT NULL,
|
|
17
|
+
"approver_id" uuid NOT NULL,
|
|
18
|
+
"on_behalf_of_id" uuid,
|
|
19
|
+
"decision" text NOT NULL,
|
|
20
|
+
"comment" text,
|
|
21
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
22
|
+
);
|
|
23
|
+
--> statement-breakpoint
|
|
24
|
+
CREATE TABLE "mod_hr"."approval_requests" (
|
|
25
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
26
|
+
"workspace_id" uuid NOT NULL,
|
|
27
|
+
"subject_type" text NOT NULL,
|
|
28
|
+
"subject_id" uuid NOT NULL,
|
|
29
|
+
"summary" text DEFAULT '' NOT NULL,
|
|
30
|
+
"chain" jsonb NOT NULL,
|
|
31
|
+
"status" text DEFAULT 'pending' NOT NULL,
|
|
32
|
+
"current_step" integer DEFAULT 0 NOT NULL,
|
|
33
|
+
"requested_by" uuid,
|
|
34
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
35
|
+
"decided_at" timestamp with time zone,
|
|
36
|
+
"version" integer DEFAULT 0 NOT NULL
|
|
37
|
+
);
|
|
38
|
+
--> statement-breakpoint
|
|
39
|
+
CREATE TABLE "mod_hr"."approval_steps" (
|
|
40
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
41
|
+
"workspace_id" uuid NOT NULL,
|
|
42
|
+
"request_id" uuid NOT NULL,
|
|
43
|
+
"step_index" integer NOT NULL,
|
|
44
|
+
"name" text DEFAULT '' NOT NULL,
|
|
45
|
+
"mode" text DEFAULT 'any' NOT NULL,
|
|
46
|
+
"min_approvals" integer DEFAULT 1 NOT NULL,
|
|
47
|
+
"approver_ids" uuid[] DEFAULT '{}'::uuid[] NOT NULL,
|
|
48
|
+
"status" text DEFAULT 'pending' NOT NULL,
|
|
49
|
+
"due_at" timestamp with time zone,
|
|
50
|
+
"escalated_at" timestamp with time zone
|
|
51
|
+
);
|
|
52
|
+
--> statement-breakpoint
|
|
53
|
+
CREATE TABLE "mod_hr"."delegations" (
|
|
54
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
55
|
+
"workspace_id" uuid NOT NULL,
|
|
56
|
+
"from_person_id" uuid NOT NULL,
|
|
57
|
+
"to_person_id" uuid NOT NULL,
|
|
58
|
+
"subject_type" text,
|
|
59
|
+
"starts_on" date NOT NULL,
|
|
60
|
+
"ends_on" date NOT NULL,
|
|
61
|
+
"reason" text,
|
|
62
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
63
|
+
);
|
|
64
|
+
--> statement-breakpoint
|
|
65
|
+
CREATE TABLE "mod_hr"."leave_balance_cursor" (
|
|
66
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
67
|
+
"workspace_id" uuid NOT NULL,
|
|
68
|
+
"person_id" uuid NOT NULL,
|
|
69
|
+
"leave_type_id" uuid NOT NULL,
|
|
70
|
+
"period_year" integer NOT NULL,
|
|
71
|
+
"cached_balance_minutes" integer DEFAULT 0 NOT NULL,
|
|
72
|
+
"as_of_entry_id" uuid,
|
|
73
|
+
"version" integer DEFAULT 0 NOT NULL,
|
|
74
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
75
|
+
);
|
|
76
|
+
--> statement-breakpoint
|
|
77
|
+
CREATE TABLE "mod_hr"."leave_ledger" (
|
|
78
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
79
|
+
"workspace_id" uuid NOT NULL,
|
|
80
|
+
"person_id" uuid NOT NULL,
|
|
81
|
+
"leave_type_id" uuid NOT NULL,
|
|
82
|
+
"kind" text NOT NULL,
|
|
83
|
+
"amount_minutes" integer NOT NULL,
|
|
84
|
+
"effective_on" date NOT NULL,
|
|
85
|
+
"period_year" integer NOT NULL,
|
|
86
|
+
"request_id" uuid,
|
|
87
|
+
"reverses_entry_id" uuid,
|
|
88
|
+
"policy_hash" text,
|
|
89
|
+
"reason" text,
|
|
90
|
+
"created_by" uuid,
|
|
91
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
92
|
+
);
|
|
93
|
+
--> statement-breakpoint
|
|
94
|
+
CREATE TABLE "mod_hr"."leave_request_days" (
|
|
95
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
96
|
+
"workspace_id" uuid NOT NULL,
|
|
97
|
+
"request_id" uuid NOT NULL,
|
|
98
|
+
"person_id" uuid NOT NULL,
|
|
99
|
+
"date" date NOT NULL,
|
|
100
|
+
"fraction" numeric(3, 2) DEFAULT '1' NOT NULL,
|
|
101
|
+
"counted" boolean DEFAULT true NOT NULL,
|
|
102
|
+
"status" text DEFAULT 'pending' NOT NULL
|
|
103
|
+
);
|
|
104
|
+
--> statement-breakpoint
|
|
105
|
+
CREATE TABLE "mod_hr"."leave_requests" (
|
|
106
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
107
|
+
"workspace_id" uuid NOT NULL,
|
|
108
|
+
"person_id" uuid NOT NULL,
|
|
109
|
+
"leave_type_id" uuid NOT NULL,
|
|
110
|
+
"starts_on" date NOT NULL,
|
|
111
|
+
"ends_on" date NOT NULL,
|
|
112
|
+
"start_part" text DEFAULT 'full' NOT NULL,
|
|
113
|
+
"end_part" text DEFAULT 'full' NOT NULL,
|
|
114
|
+
"hours" numeric(5, 2),
|
|
115
|
+
"working_days" numeric(6, 2) DEFAULT '0' NOT NULL,
|
|
116
|
+
"minutes" integer DEFAULT 0 NOT NULL,
|
|
117
|
+
"status" text DEFAULT 'pending' NOT NULL,
|
|
118
|
+
"reason" text,
|
|
119
|
+
"document_file_id" uuid,
|
|
120
|
+
"approval_request_id" uuid,
|
|
121
|
+
"idempotency_key" text,
|
|
122
|
+
"decided_at" timestamp with time zone,
|
|
123
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
124
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
125
|
+
);
|
|
126
|
+
--> statement-breakpoint
|
|
127
|
+
CREATE TABLE "mod_hr"."leave_types" (
|
|
128
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
129
|
+
"workspace_id" uuid NOT NULL,
|
|
130
|
+
"key" text NOT NULL,
|
|
131
|
+
"name" text NOT NULL,
|
|
132
|
+
"paid" boolean DEFAULT true NOT NULL,
|
|
133
|
+
"unit" text DEFAULT 'day' NOT NULL,
|
|
134
|
+
"color" text,
|
|
135
|
+
"icon" text,
|
|
136
|
+
"requires_document_after_days" integer,
|
|
137
|
+
"counts_working_days_only" boolean DEFAULT true NOT NULL,
|
|
138
|
+
"allow_negative" boolean DEFAULT false NOT NULL,
|
|
139
|
+
"max_negative_minutes" integer DEFAULT 0 NOT NULL,
|
|
140
|
+
"order" integer DEFAULT 0 NOT NULL,
|
|
141
|
+
"archived_at" timestamp with time zone,
|
|
142
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
143
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
144
|
+
);
|
|
145
|
+
--> statement-breakpoint
|
|
146
|
+
CREATE INDEX "hr_approval_chains_idx" ON "mod_hr"."approval_chains" USING btree ("workspace_id","subject_type","archived_at");--> statement-breakpoint
|
|
147
|
+
CREATE UNIQUE INDEX "hr_approval_decisions_uq" ON "mod_hr"."approval_decisions" USING btree ("step_id","approver_id");--> statement-breakpoint
|
|
148
|
+
CREATE INDEX "hr_approval_requests_subject_idx" ON "mod_hr"."approval_requests" USING btree ("workspace_id","subject_type","subject_id");--> statement-breakpoint
|
|
149
|
+
CREATE INDEX "hr_approval_requests_status_idx" ON "mod_hr"."approval_requests" USING btree ("workspace_id","status");--> statement-breakpoint
|
|
150
|
+
CREATE UNIQUE INDEX "hr_approval_steps_uq" ON "mod_hr"."approval_steps" USING btree ("request_id","step_index");--> statement-breakpoint
|
|
151
|
+
CREATE INDEX "hr_approval_steps_due_idx" ON "mod_hr"."approval_steps" USING btree ("workspace_id","status","due_at");--> statement-breakpoint
|
|
152
|
+
CREATE INDEX "hr_delegations_idx" ON "mod_hr"."delegations" USING btree ("workspace_id","to_person_id","starts_on");--> statement-breakpoint
|
|
153
|
+
CREATE UNIQUE INDEX "hr_balance_cursor_uq" ON "mod_hr"."leave_balance_cursor" USING btree ("workspace_id","person_id","leave_type_id","period_year");--> statement-breakpoint
|
|
154
|
+
CREATE INDEX "hr_ledger_person_idx" ON "mod_hr"."leave_ledger" USING btree ("workspace_id","person_id","leave_type_id","effective_on");--> statement-breakpoint
|
|
155
|
+
CREATE INDEX "hr_ledger_request_idx" ON "mod_hr"."leave_ledger" USING btree ("workspace_id","request_id");--> statement-breakpoint
|
|
156
|
+
CREATE INDEX "hr_ledger_year_idx" ON "mod_hr"."leave_ledger" USING btree ("workspace_id","period_year");--> statement-breakpoint
|
|
157
|
+
CREATE INDEX "hr_leave_days_person_idx" ON "mod_hr"."leave_request_days" USING btree ("workspace_id","person_id","date");--> statement-breakpoint
|
|
158
|
+
CREATE INDEX "hr_leave_days_request_idx" ON "mod_hr"."leave_request_days" USING btree ("request_id");--> statement-breakpoint
|
|
159
|
+
CREATE INDEX "hr_leave_requests_person_idx" ON "mod_hr"."leave_requests" USING btree ("workspace_id","person_id","starts_on");--> statement-breakpoint
|
|
160
|
+
CREATE INDEX "hr_leave_requests_status_idx" ON "mod_hr"."leave_requests" USING btree ("workspace_id","status","starts_on");--> statement-breakpoint
|
|
161
|
+
CREATE UNIQUE INDEX "hr_leave_requests_idem_uq" ON "mod_hr"."leave_requests" USING btree ("workspace_id","idempotency_key");--> statement-breakpoint
|
|
162
|
+
CREATE UNIQUE INDEX "hr_leave_types_ws_key_uq" ON "mod_hr"."leave_types" USING btree ("workspace_id","key");--> statement-breakpoint
|
|
163
|
+
-- ---------------------------------------------------------------------------------------------
|
|
164
|
+
-- Row-level security on the new tenant tables.
|
|
165
|
+
alter table "mod_hr"."leave_types" enable row level security;--> statement-breakpoint
|
|
166
|
+
alter table "mod_hr"."leave_types" force row level security;--> statement-breakpoint
|
|
167
|
+
create policy "leave_types_ws_isolation" on "mod_hr"."leave_types"
|
|
168
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
169
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
170
|
+
|
|
171
|
+
alter table "mod_hr"."leave_ledger" enable row level security;--> statement-breakpoint
|
|
172
|
+
alter table "mod_hr"."leave_ledger" force row level security;--> statement-breakpoint
|
|
173
|
+
create policy "leave_ledger_ws_isolation" on "mod_hr"."leave_ledger"
|
|
174
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
175
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
176
|
+
|
|
177
|
+
alter table "mod_hr"."leave_balance_cursor" enable row level security;--> statement-breakpoint
|
|
178
|
+
alter table "mod_hr"."leave_balance_cursor" force row level security;--> statement-breakpoint
|
|
179
|
+
create policy "leave_balance_cursor_ws_isolation" on "mod_hr"."leave_balance_cursor"
|
|
180
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
181
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
182
|
+
|
|
183
|
+
alter table "mod_hr"."leave_requests" enable row level security;--> statement-breakpoint
|
|
184
|
+
alter table "mod_hr"."leave_requests" force row level security;--> statement-breakpoint
|
|
185
|
+
create policy "leave_requests_ws_isolation" on "mod_hr"."leave_requests"
|
|
186
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
187
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
188
|
+
|
|
189
|
+
alter table "mod_hr"."leave_request_days" enable row level security;--> statement-breakpoint
|
|
190
|
+
alter table "mod_hr"."leave_request_days" force row level security;--> statement-breakpoint
|
|
191
|
+
create policy "leave_request_days_ws_isolation" on "mod_hr"."leave_request_days"
|
|
192
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
193
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
194
|
+
|
|
195
|
+
alter table "mod_hr"."approval_chains" enable row level security;--> statement-breakpoint
|
|
196
|
+
alter table "mod_hr"."approval_chains" force row level security;--> statement-breakpoint
|
|
197
|
+
create policy "approval_chains_ws_isolation" on "mod_hr"."approval_chains"
|
|
198
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
199
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
200
|
+
|
|
201
|
+
alter table "mod_hr"."approval_requests" enable row level security;--> statement-breakpoint
|
|
202
|
+
alter table "mod_hr"."approval_requests" force row level security;--> statement-breakpoint
|
|
203
|
+
create policy "approval_requests_ws_isolation" on "mod_hr"."approval_requests"
|
|
204
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
205
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
206
|
+
|
|
207
|
+
alter table "mod_hr"."approval_steps" enable row level security;--> statement-breakpoint
|
|
208
|
+
alter table "mod_hr"."approval_steps" force row level security;--> statement-breakpoint
|
|
209
|
+
create policy "approval_steps_ws_isolation" on "mod_hr"."approval_steps"
|
|
210
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
211
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
212
|
+
|
|
213
|
+
alter table "mod_hr"."approval_decisions" enable row level security;--> statement-breakpoint
|
|
214
|
+
alter table "mod_hr"."approval_decisions" force row level security;--> statement-breakpoint
|
|
215
|
+
create policy "approval_decisions_ws_isolation" on "mod_hr"."approval_decisions"
|
|
216
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
217
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
218
|
+
|
|
219
|
+
alter table "mod_hr"."delegations" enable row level security;--> statement-breakpoint
|
|
220
|
+
alter table "mod_hr"."delegations" force row level security;--> statement-breakpoint
|
|
221
|
+
create policy "delegations_ws_isolation" on "mod_hr"."delegations"
|
|
222
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
223
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
224
|
+
|
|
225
|
+
-- ---------------------------------------------------------------------------------------------
|
|
226
|
+
-- The invariant that keeps a balance honest under concurrency.
|
|
227
|
+
--
|
|
228
|
+
-- Two overlapping requests can both read the same balance, both see enough for the last day, and
|
|
229
|
+
-- both succeed — leaving somebody minus a day and nobody able to say which request caused it. The
|
|
230
|
+
-- cursor lock serialises the *spend*; this index refuses the *overlap*, so a person cannot hold two
|
|
231
|
+
-- live requests covering one date whatever the application layer believes.
|
|
232
|
+
--
|
|
233
|
+
-- Partial on purpose: a cancelled or rejected request must not block rebooking the same day, and a
|
|
234
|
+
-- weekend inside a range is not counted so it does not conflict with anything either.
|
|
235
|
+
create unique index "hr_leave_days_no_double_booking"
|
|
236
|
+
on "mod_hr"."leave_request_days" (workspace_id, person_id, date)
|
|
237
|
+
where counted and status in ('pending', 'approved');
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
CREATE TABLE "mod_hr"."attendance_days" (
|
|
2
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
3
|
+
"workspace_id" uuid NOT NULL,
|
|
4
|
+
"person_id" uuid NOT NULL,
|
|
5
|
+
"business_date" date NOT NULL,
|
|
6
|
+
"scheduled_minutes" integer DEFAULT 0 NOT NULL,
|
|
7
|
+
"worked_minutes" integer DEFAULT 0 NOT NULL,
|
|
8
|
+
"break_minutes" integer DEFAULT 0 NOT NULL,
|
|
9
|
+
"overtime_minutes" integer DEFAULT 0 NOT NULL,
|
|
10
|
+
"late_minutes" integer DEFAULT 0 NOT NULL,
|
|
11
|
+
"early_leave_minutes" integer DEFAULT 0 NOT NULL,
|
|
12
|
+
"status" text DEFAULT 'absent' NOT NULL,
|
|
13
|
+
"leave_request_id" uuid,
|
|
14
|
+
"anomalies" text[] DEFAULT '{}'::text[] NOT NULL,
|
|
15
|
+
"first_in" timestamp with time zone,
|
|
16
|
+
"last_out" timestamp with time zone,
|
|
17
|
+
"policy_hash" text,
|
|
18
|
+
"locked" boolean DEFAULT false NOT NULL,
|
|
19
|
+
"computed_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
20
|
+
);
|
|
21
|
+
--> statement-breakpoint
|
|
22
|
+
CREATE TABLE "mod_hr"."punches" (
|
|
23
|
+
"id" uuid DEFAULT uuidv7() NOT NULL,
|
|
24
|
+
"workspace_id" uuid NOT NULL,
|
|
25
|
+
"person_id" uuid NOT NULL,
|
|
26
|
+
"direction" text NOT NULL,
|
|
27
|
+
"at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
28
|
+
"client_reported_at" timestamp with time zone,
|
|
29
|
+
"skew_ms" integer,
|
|
30
|
+
"business_date" date NOT NULL,
|
|
31
|
+
"timezone" text DEFAULT 'UTC' NOT NULL,
|
|
32
|
+
"method" text DEFAULT 'web' NOT NULL,
|
|
33
|
+
"office_id" uuid,
|
|
34
|
+
"device_id" uuid,
|
|
35
|
+
"geo" jsonb,
|
|
36
|
+
"trust" text DEFAULT 'trusted' NOT NULL,
|
|
37
|
+
"voided_by_punch_id" uuid,
|
|
38
|
+
"idempotency_key" text,
|
|
39
|
+
"note" text,
|
|
40
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
41
|
+
) PARTITION BY RANGE ("business_date");
|
|
42
|
+
--> statement-breakpoint
|
|
43
|
+
-- Partitioned, because five hundred people punching four times a day is half a million rows a year
|
|
44
|
+
-- and retrofitting partitioning onto a live table of those is a migration nobody wants. drizzle-kit
|
|
45
|
+
-- cannot express PARTITION BY, so this block is hand-maintained: **after `pnpm db:generate`,
|
|
46
|
+
-- re-add the `PARTITION BY`, the primary key and the partitions below.**
|
|
47
|
+
--
|
|
48
|
+
-- A partitioned table's primary key must contain the partition column, which is why it is
|
|
49
|
+
-- (id, business_date) rather than (id). Same for the idempotency index: without business_date in it
|
|
50
|
+
-- Postgres refuses to create it at all.
|
|
51
|
+
ALTER TABLE "mod_hr"."punches" ADD PRIMARY KEY ("id", "business_date");
|
|
52
|
+
--> statement-breakpoint
|
|
53
|
+
CREATE UNIQUE INDEX "hr_punches_idem_uq"
|
|
54
|
+
ON "mod_hr"."punches" ("workspace_id", "idempotency_key", "business_date")
|
|
55
|
+
WHERE "idempotency_key" IS NOT NULL;
|
|
56
|
+
--> statement-breakpoint
|
|
57
|
+
-- A DEFAULT partition catches anything outside the ranges below, so a missing monthly partition
|
|
58
|
+
-- degrades to a slow insert rather than a failed punch. Attendance that refuses to record because
|
|
59
|
+
-- a maintenance job did not run is worse than attendance that records into the wrong file.
|
|
60
|
+
CREATE TABLE IF NOT EXISTS "mod_hr"."punches_default"
|
|
61
|
+
PARTITION OF "mod_hr"."punches" DEFAULT;
|
|
62
|
+
--> statement-breakpoint
|
|
63
|
+
ALTER TABLE "mod_hr"."punches_default" ENABLE ROW LEVEL SECURITY;
|
|
64
|
+
--> statement-breakpoint
|
|
65
|
+
ALTER TABLE "mod_hr"."punches_default" FORCE ROW LEVEL SECURITY;
|
|
66
|
+
--> statement-breakpoint
|
|
67
|
+
CREATE POLICY "punches_ws_isolation" ON "mod_hr"."punches_default"
|
|
68
|
+
USING (workspace_id::text = current_setting('app.workspace_id', true))
|
|
69
|
+
WITH CHECK (workspace_id::text = current_setting('app.workspace_id', true));
|
|
70
|
+
--> statement-breakpoint
|
|
71
|
+
-- Creating a partition is not just `CREATE TABLE ... PARTITION OF`.
|
|
72
|
+
--
|
|
73
|
+
-- A policy on a partitioned parent applies when you query **through the parent**. Query a partition
|
|
74
|
+
-- directly and its own row-level security setting applies — and a fresh partition has none. Any
|
|
75
|
+
-- deployment that runs `GRANT SELECT ON ALL TABLES IN SCHEMA mod_hr` therefore hands out an
|
|
76
|
+
-- unfiltered view of one month's punches, silently, to every role that grant touched.
|
|
77
|
+
--
|
|
78
|
+
-- So partition creation goes through this function, which secures what it creates. The monthly job
|
|
79
|
+
-- calls the same function, which is the only way it cannot forget.
|
|
80
|
+
CREATE OR REPLACE FUNCTION "mod_hr".ensure_punch_partition(month_start date)
|
|
81
|
+
RETURNS text
|
|
82
|
+
LANGUAGE plpgsql
|
|
83
|
+
AS $fn$
|
|
84
|
+
DECLARE
|
|
85
|
+
part_name text := 'punches_' || to_char(month_start, 'YYYY_MM');
|
|
86
|
+
BEGIN
|
|
87
|
+
EXECUTE format(
|
|
88
|
+
'CREATE TABLE IF NOT EXISTS %I.%I PARTITION OF %I.%I FOR VALUES FROM (%L) TO (%L)',
|
|
89
|
+
'mod_hr', part_name, 'mod_hr', 'punches', month_start, (month_start + interval '1 month')::date
|
|
90
|
+
);
|
|
91
|
+
EXECUTE format('ALTER TABLE %I.%I ENABLE ROW LEVEL SECURITY', 'mod_hr', part_name);
|
|
92
|
+
EXECUTE format('ALTER TABLE %I.%I FORCE ROW LEVEL SECURITY', 'mod_hr', part_name);
|
|
93
|
+
-- `IF NOT EXISTS` on CREATE POLICY needs Postgres 18; the catalogue check works everywhere and
|
|
94
|
+
-- keeps this function safe to call repeatedly, which the monthly job relies on.
|
|
95
|
+
IF NOT EXISTS (
|
|
96
|
+
SELECT 1 FROM pg_policies
|
|
97
|
+
WHERE schemaname = 'mod_hr' AND tablename = part_name AND policyname = 'punches_ws_isolation'
|
|
98
|
+
) THEN
|
|
99
|
+
EXECUTE format(
|
|
100
|
+
'CREATE POLICY "punches_ws_isolation" ON %I.%I
|
|
101
|
+
USING (workspace_id::text = current_setting(''app.workspace_id'', true))
|
|
102
|
+
WITH CHECK (workspace_id::text = current_setting(''app.workspace_id'', true))',
|
|
103
|
+
'mod_hr', part_name
|
|
104
|
+
);
|
|
105
|
+
END IF;
|
|
106
|
+
RETURN part_name;
|
|
107
|
+
END
|
|
108
|
+
$fn$;
|
|
109
|
+
--> statement-breakpoint
|
|
110
|
+
-- Seeds a window either side of today so a fresh instance can record punches immediately.
|
|
111
|
+
-- `hr.ensure-partitions` keeps it rolling.
|
|
112
|
+
DO $$
|
|
113
|
+
DECLARE
|
|
114
|
+
m date := (date_trunc('month', now()) - interval '3 months')::date;
|
|
115
|
+
stop date := (date_trunc('month', now()) + interval '12 months')::date;
|
|
116
|
+
BEGIN
|
|
117
|
+
WHILE m < stop LOOP
|
|
118
|
+
PERFORM "mod_hr".ensure_punch_partition(m);
|
|
119
|
+
m := (m + interval '1 month')::date;
|
|
120
|
+
END LOOP;
|
|
121
|
+
END $$;
|
|
122
|
+
--> statement-breakpoint
|
|
123
|
+
CREATE TABLE "mod_hr"."regularizations" (
|
|
124
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
125
|
+
"workspace_id" uuid NOT NULL,
|
|
126
|
+
"person_id" uuid NOT NULL,
|
|
127
|
+
"business_date" date NOT NULL,
|
|
128
|
+
"punch_id" uuid,
|
|
129
|
+
"proposed" jsonb NOT NULL,
|
|
130
|
+
"reason" text NOT NULL,
|
|
131
|
+
"status" text DEFAULT 'pending' NOT NULL,
|
|
132
|
+
"approval_request_id" uuid,
|
|
133
|
+
"applied_at" timestamp with time zone,
|
|
134
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
135
|
+
);
|
|
136
|
+
--> statement-breakpoint
|
|
137
|
+
CREATE TABLE "mod_hr"."schedule_assignments" (
|
|
138
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
139
|
+
"workspace_id" uuid NOT NULL,
|
|
140
|
+
"person_id" uuid NOT NULL,
|
|
141
|
+
"schedule_id" uuid NOT NULL,
|
|
142
|
+
"effective_from" date NOT NULL,
|
|
143
|
+
"effective_to" date,
|
|
144
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
145
|
+
);
|
|
146
|
+
--> statement-breakpoint
|
|
147
|
+
CREATE TABLE "mod_hr"."schedules" (
|
|
148
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
149
|
+
"workspace_id" uuid NOT NULL,
|
|
150
|
+
"name" text NOT NULL,
|
|
151
|
+
"kind" text DEFAULT 'fixed' NOT NULL,
|
|
152
|
+
"week" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
153
|
+
"tz_mode" text DEFAULT 'office' NOT NULL,
|
|
154
|
+
"tz" text,
|
|
155
|
+
"grace_in_minutes" integer DEFAULT 0 NOT NULL,
|
|
156
|
+
"grace_out_minutes" integer DEFAULT 0 NOT NULL,
|
|
157
|
+
"rounding_step_minutes" integer DEFAULT 0 NOT NULL,
|
|
158
|
+
"rounding_direction" text DEFAULT 'nearest' NOT NULL,
|
|
159
|
+
"auto_clock_out_after_minutes" integer,
|
|
160
|
+
"archived_at" timestamp with time zone,
|
|
161
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
162
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
163
|
+
);
|
|
164
|
+
--> statement-breakpoint
|
|
165
|
+
CREATE UNIQUE INDEX "hr_attendance_days_uq" ON "mod_hr"."attendance_days" USING btree ("workspace_id","person_id","business_date");--> statement-breakpoint
|
|
166
|
+
CREATE INDEX "hr_attendance_days_date_idx" ON "mod_hr"."attendance_days" USING btree ("workspace_id","business_date","status");--> statement-breakpoint
|
|
167
|
+
CREATE INDEX "hr_punches_person_idx" ON "mod_hr"."punches" USING btree ("workspace_id","person_id","business_date");--> statement-breakpoint
|
|
168
|
+
CREATE INDEX "hr_regularizations_idx" ON "mod_hr"."regularizations" USING btree ("workspace_id","person_id","business_date");--> statement-breakpoint
|
|
169
|
+
CREATE INDEX "hr_schedule_assign_idx" ON "mod_hr"."schedule_assignments" USING btree ("workspace_id","person_id","effective_from");--> statement-breakpoint
|
|
170
|
+
CREATE INDEX "hr_schedules_ws_idx" ON "mod_hr"."schedules" USING btree ("workspace_id","archived_at");--> statement-breakpoint
|
|
171
|
+
-- ---------------------------------------------------------------------------------------------
|
|
172
|
+
-- Row-level security on the attendance tables.
|
|
173
|
+
--
|
|
174
|
+
-- A policy on a partitioned parent applies to every partition, including ones created later — which
|
|
175
|
+
-- is what makes the rolling partition job safe. The test asserts a freshly created partition
|
|
176
|
+
-- returns nothing without `app.workspace_id`, because "it inherits" is the kind of thing that is
|
|
177
|
+
-- true until a Postgres upgrade says otherwise.
|
|
178
|
+
alter table "mod_hr"."schedules" enable row level security;--> statement-breakpoint
|
|
179
|
+
alter table "mod_hr"."schedules" force row level security;--> statement-breakpoint
|
|
180
|
+
create policy "schedules_ws_isolation" on "mod_hr"."schedules"
|
|
181
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
182
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
183
|
+
|
|
184
|
+
alter table "mod_hr"."schedule_assignments" enable row level security;--> statement-breakpoint
|
|
185
|
+
alter table "mod_hr"."schedule_assignments" force row level security;--> statement-breakpoint
|
|
186
|
+
create policy "schedule_assignments_ws_isolation" on "mod_hr"."schedule_assignments"
|
|
187
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
188
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
189
|
+
|
|
190
|
+
alter table "mod_hr"."punches" enable row level security;--> statement-breakpoint
|
|
191
|
+
alter table "mod_hr"."punches" force row level security;--> statement-breakpoint
|
|
192
|
+
create policy "punches_ws_isolation" on "mod_hr"."punches"
|
|
193
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
194
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
195
|
+
|
|
196
|
+
alter table "mod_hr"."attendance_days" enable row level security;--> statement-breakpoint
|
|
197
|
+
alter table "mod_hr"."attendance_days" force row level security;--> statement-breakpoint
|
|
198
|
+
create policy "attendance_days_ws_isolation" on "mod_hr"."attendance_days"
|
|
199
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
200
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
201
|
+
|
|
202
|
+
alter table "mod_hr"."regularizations" enable row level security;--> statement-breakpoint
|
|
203
|
+
alter table "mod_hr"."regularizations" force row level security;--> statement-breakpoint
|
|
204
|
+
create policy "regularizations_ws_isolation" on "mod_hr"."regularizations"
|
|
205
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
206
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));
|