@kernhq/module-hr 0.13.1 → 0.13.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,QAAQ;;;;;iCA6HnB,CAAA;AAkBF,eAAe,QAAQ,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,QAAQ;;;;;iCA4JnB,CAAA;AAkBF,eAAe,QAAQ,CAAA"}
@@ -109,6 +109,36 @@ export const hrModule = defineServerModule({
109
109
  return row ?? null;
110
110
  }),
111
111
  },
112
+ /**
113
+ * The same person, the other way round: a `personId` to the account behind it.
114
+ *
115
+ * Every HR event carries a `personId` and none of them carries a `userId`, because a person is
116
+ * HR's noun and an account is core's — so a subscriber that has to act on somebody's *account*
117
+ * (revoke a licence, collect the laptop the asset register says is theirs) has a person id and
118
+ * nothing it can do with one. `person.byUserId` answers the question a module asks when it
119
+ * already knows the account; this answers the one it asks when it has just been told an
120
+ * employment ended.
121
+ *
122
+ * `userId` is nullable and legitimately null twice over: for somebody with no Kern account, and
123
+ * for somebody whose account was removed from the workspace — `core.member.removed` above
124
+ * clears the link on purpose, because an employment record outlives an account. A caller must
125
+ * treat null as "nothing to do", not as an error.
126
+ */
127
+ 'person.get': {
128
+ handler: async (input, { kernel }) => kernel.database.withWorkspace(input.workspaceId, async (tx) => {
129
+ const [row] = await tx
130
+ .select({
131
+ id: people.id,
132
+ userId: people.userId,
133
+ displayName: people.displayName,
134
+ status: people.status,
135
+ })
136
+ .from(people)
137
+ .where(eq(people.id, input.personId))
138
+ .limit(1);
139
+ return row ?? null;
140
+ }),
141
+ },
112
142
  },
113
143
  subscriptions: {
114
144
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/server/router.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,cAAc,EAMpB,MAAM,gBAAgB,CAAA;AA0DvB;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAw4HxC"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/server/router.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,cAAc,EAMpB,MAAM,gBAAgB,CAAA;AAmKvB;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+jIxC"}
@@ -1,6 +1,6 @@
1
1
  import { KernError, requires, requiresCapability, uuidv7, workspaceScoped, } from '@kernhq/kernel';
2
2
  import { implement } from '@orpc/server';
3
- import { and, asc, count, desc, eq, gte, ilike, inArray, isNull, lte, or, sql } from 'drizzle-orm';
3
+ import { and, asc, count, desc, eq, getTableColumns, gte, ilike, inArray, isNull, lte, or, sql, } from 'drizzle-orm';
4
4
  import { HrSettings, hrContract, hrEvents, MODULE_ID } from '../contract/index.js';
5
5
  import { AccrualConfig, CarryForwardConfig, OvertimeConfig, RoundingConfig, WorkingTimeConfig, } from '../contract/policies.js';
6
6
  import { accrueForPeriod } from '../policy/accrual.js';
@@ -15,6 +15,74 @@ import { PeopleService } from './services/people.js';
15
15
  import { hashConfig, PolicyService } from './services/policies.js';
16
16
  import { DEFAULT_WORKING_WEEK, ResolveService } from './services/resolve.js';
17
17
  const os = implement(hrContract).$context();
18
+ const encodeCursor = (key, id) => Buffer.from(JSON.stringify([key, id]), 'utf8').toString('base64url');
19
+ function decodeCursor(raw) {
20
+ if (!raw)
21
+ return null;
22
+ try {
23
+ const [key, id] = JSON.parse(Buffer.from(raw, 'base64url').toString('utf8'));
24
+ if (typeof key !== 'string' || typeof id !== 'string')
25
+ throw new Error('malformed cursor');
26
+ return { key, id };
27
+ }
28
+ catch {
29
+ // A cursor is opaque to the caller, so a broken one is a tampered URL or a bug on our side —
30
+ // never something the reader can fix by asking again. Refusing beats quietly serving page one,
31
+ // which reads as a list that jumps back to the top for no reason anybody can see.
32
+ throw KernError.badRequest('That page cursor is not valid.');
33
+ }
34
+ }
35
+ /**
36
+ * `(sort, id) > (…)` as one row comparison rather than `sort > k or (sort = k and id > i)`:
37
+ * Postgres can drive a row comparison straight off an index on `(sort, id)` and cannot do that with
38
+ * the `or` spelling. Both halves of the cursor are cast to the column's own type, because an
39
+ * untyped bind parameter beside a `date` or a `timestamptz` is not something to make the planner
40
+ * guess at.
41
+ */
42
+ const after = (sort, id, dir, c) => {
43
+ const from = sql `(${c.key}::${sql.raw(sort.getSQLType())}, ${c.id}::uuid)`;
44
+ return dir === 'asc' ? sql `(${sort}, ${id}) > ${from}` : sql `(${sort}, ${id}) < ${from}`;
45
+ };
46
+ /**
47
+ * Did this error come from one named unique index?
48
+ *
49
+ * The cause chain is walked rather than the error itself, because drizzle wraps what the driver
50
+ * threw and the `code` a duplicate key arrives as — `23505` — is on the pg error underneath. The
51
+ * index name is checked too: a handler that treats *any* duplicate key as its own idempotency
52
+ * replay would silently swallow a collision on some other constraint, which is a bug reported as a
53
+ * success.
54
+ */
55
+ function isUniqueViolation(err, constraint) {
56
+ for (let e = err, depth = 0; e && depth < 5; e = e.cause, depth++) {
57
+ const pg = e;
58
+ if (pg.code === '23505' && pg.constraint === constraint)
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+ /**
64
+ * Cut a page out of the `limit + 1` rows the query asked for.
65
+ *
66
+ * The extra row is how "there is more" is known without a second count, and why a page that happens
67
+ * to fill exactly does not advertise a next page that turns out to be empty.
68
+ *
69
+ * **The key is `string`, never a `Date`, and that is load-bearing.** A `timestamptz` is stored at
70
+ * microsecond precision and node-postgres hands it back as a JS `Date`, which is milliseconds — so
71
+ * a cursor built from the row object is *strictly less* than the value it came from, and
72
+ * `(at, id) < (key, id)` then excludes every row that ties with the last row of the page. That is
73
+ * silent: no error, the list just ends early, and it bites hardest exactly where the id tiebreaker
74
+ * was supposed to save it — one edit writing several rows in a transaction, all sharing `now()`.
75
+ * Measured on `person_history`: five rows in one statement, page size two, page two returned none
76
+ * of the remaining three. A timestamp cursor therefore has to select the value `::text` and pass
77
+ * that, which the type here forces rather than trusts.
78
+ */
79
+ function paginate(rows, limit, cursorOf) {
80
+ if (rows.length <= limit)
81
+ return { items: rows, nextCursor: null };
82
+ const items = rows.slice(0, limit);
83
+ const [key, id] = cursorOf(items[items.length - 1]);
84
+ return { items, nextCursor: encodeCursor(key, id) };
85
+ }
18
86
  /**
19
87
  * The router.
20
88
  *
@@ -72,16 +140,21 @@ export function implement_(kernel) {
72
140
  ? inArray(people.id, holders.map((r) => r.personId))
73
141
  : sql `false`);
74
142
  }
75
- const rows = await tx
76
- .select()
77
- .from(people)
78
- .where(and(...where))
79
- .orderBy(asc(people.displayName))
80
- .limit(input.limit);
143
+ // The count is of everything the filters match, so it is taken before the cursor narrows
144
+ // the set — `total` is the size of the directory, not of the page being looked at.
81
145
  const [total] = await tx
82
146
  .select({ n: count() })
83
147
  .from(people)
84
148
  .where(and(...where));
149
+ const cursor = decodeCursor(input.cursor);
150
+ if (cursor)
151
+ where.push(after(people.displayName, people.id, 'asc', cursor));
152
+ const { items: rows, nextCursor } = paginate(await tx
153
+ .select()
154
+ .from(people)
155
+ .where(and(...where))
156
+ .orderBy(asc(people.displayName), asc(people.id))
157
+ .limit(input.limit + 1), input.limit, (r) => [r.displayName, r.id]);
85
158
  // One query for the whole page rather than a resolution per row: a directory of five
86
159
  // hundred people would otherwise be five hundred ladder walks.
87
160
  const assignments = rows.length
@@ -105,7 +178,7 @@ export function implement_(kernel) {
105
178
  officeId: officeBy.get(r.id)?.officeId ?? null,
106
179
  officeName: officeBy.get(r.id)?.name ?? null,
107
180
  })),
108
- nextCursor: null,
181
+ nextCursor,
109
182
  total: total?.n ?? 0,
110
183
  };
111
184
  })),
@@ -186,7 +259,15 @@ export function implement_(kernel) {
186
259
  await changed(workspaceId, 'person', personId, 'updated');
187
260
  return PeopleService.toPerson(row.updated);
188
261
  }),
189
- /** Ends employment and keeps the record. A terminated person is history, not a deletion. */
262
+ /**
263
+ * Ends employment and keeps the record. A terminated person is history, not a deletion.
264
+ *
265
+ * The reason the dialog collects is written to `person_history` rather than to a column on
266
+ * `people`: "why did she leave" is a fact about the *event*, it is asked for by an audit
267
+ * alongside who ended the employment and when, and `person_history` is the append-only table
268
+ * that already answers exactly that question. It used to be accepted and dropped on the
269
+ * floor, which is the one thing an offboarding record must not do.
270
+ */
190
271
  offboard: scoped.people.offboard
191
272
  .use(requires('hr.person.manage'))
192
273
  .handler(async ({ input, context }) => {
@@ -205,9 +286,13 @@ export function implement_(kernel) {
205
286
  .update(officeAssignments)
206
287
  .set({ effectiveTo: input.on })
207
288
  .where(and(eq(officeAssignments.workspaceId, input.workspaceId), eq(officeAssignments.personId, input.personId), isNull(officeAssignments.effectiveTo)));
289
+ const reason = input.reason?.trim();
208
290
  await svc.record(tx, input.workspaceId, input.personId, context.principal.userId ?? null, [
209
291
  { field: 'status', from: before.status, to: 'terminated' },
210
292
  { field: 'terminatedOn', from: before.terminatedOn, to: input.on },
293
+ // Only when there is one. A row saying the reason changed from nothing to nothing is
294
+ // noise in the trail somebody reads to find out what happened.
295
+ ...(reason ? [{ field: 'terminationReason', from: null, to: reason }] : []),
211
296
  ]);
212
297
  return { before, updated: updated };
213
298
  });
@@ -222,12 +307,25 @@ export function implement_(kernel) {
222
307
  return PeopleService.toPerson(row.updated);
223
308
  }),
224
309
  history: scoped.people.history.use(requires('hr.person.view')).handler(({ input }) => db.withWorkspace(input.workspaceId, async (tx) => {
225
- const rows = await tx
226
- .select()
310
+ const where = [
311
+ eq(personHistory.workspaceId, input.workspaceId),
312
+ eq(personHistory.personId, input.personId),
313
+ ];
314
+ // The id tiebreaker earns its keep here more than anywhere: one edit writes several rows
315
+ // in one statement, and `now()` is frozen for a transaction, so they all share an `at`.
316
+ const cursor = decodeCursor(input.cursor);
317
+ if (cursor)
318
+ where.push(after(personHistory.at, personHistory.id, 'desc', cursor));
319
+ const { items: rows, nextCursor } = paginate(
320
+ // `atText` rather than the `Date`: see `paginate`. `now()` is frozen for a transaction,
321
+ // so a single edit's rows all share an `at` to the microsecond, and a millisecond cursor
322
+ // would drop every one of them after the first page.
323
+ await tx
324
+ .select({ ...getTableColumns(personHistory), atText: sql `${personHistory.at}::text` })
227
325
  .from(personHistory)
228
- .where(and(eq(personHistory.workspaceId, input.workspaceId), eq(personHistory.personId, input.personId)))
229
- .orderBy(desc(personHistory.at))
230
- .limit(input.limit);
326
+ .where(and(...where))
327
+ .orderBy(desc(personHistory.at), desc(personHistory.id))
328
+ .limit(input.limit + 1), input.limit, (r) => [r.atText, r.id]);
231
329
  return {
232
330
  items: rows.map((r) => ({
233
331
  id: r.id,
@@ -238,7 +336,7 @@ export function implement_(kernel) {
238
336
  actorId: r.actorId,
239
337
  source: r.source,
240
338
  })),
241
- nextCursor: null,
339
+ nextCursor,
242
340
  };
243
341
  })),
244
342
  sensitive: {
@@ -678,10 +776,20 @@ export function implement_(kernel) {
678
776
  await changed(input.workspaceId, 'office', row.id, 'updated');
679
777
  return toOffice(row);
680
778
  }),
779
+ /**
780
+ * The office's people, and its headcount.
781
+ *
782
+ * One join rather than the two round trips this used to be. The old shape limited the
783
+ * *assignments* and then sorted the people it had fetched, so a page was an arbitrary subset
784
+ * put in alphabetical order — and it reported `rows.length` as `total`, which told an office
785
+ * of forty that it had twenty as soon as one page stopped holding everybody. The headcount is
786
+ * counted now, over the same predicate and without the cursor.
787
+ */
681
788
  people: scoped.offices.people
682
789
  .use(cap('offices'))
683
790
  .use(requires('hr.office.view'))
684
791
  .handler(({ input }) => db.withWorkspace(input.workspaceId, async (tx) => {
792
+ const here = and(eq(people.id, officeAssignments.personId), eq(people.workspaceId, officeAssignments.workspaceId));
685
793
  const where = [
686
794
  eq(officeAssignments.workspaceId, input.workspaceId),
687
795
  eq(officeAssignments.officeId, input.officeId),
@@ -689,26 +797,30 @@ export function implement_(kernel) {
689
797
  ];
690
798
  if (input.primaryOnly)
691
799
  where.push(eq(officeAssignments.isPrimary, true));
692
- const assignments = await tx
693
- .select()
800
+ const [total] = await tx
801
+ .select({ n: count() })
802
+ .from(officeAssignments)
803
+ .innerJoin(people, here)
804
+ .where(and(...where));
805
+ const cursor = decodeCursor(input.cursor);
806
+ if (cursor)
807
+ where.push(after(people.displayName, people.id, 'asc', cursor));
808
+ const { items: rows, nextCursor } = paginate(await tx
809
+ .select({ person: people, isPrimary: officeAssignments.isPrimary })
694
810
  .from(officeAssignments)
811
+ .innerJoin(people, here)
695
812
  .where(and(...where))
696
- .limit(input.limit);
697
- if (!assignments.length)
698
- return { items: [], nextCursor: null, total: 0 };
699
- const rows = await tx
700
- .select()
701
- .from(people)
702
- .where(and(eq(people.workspaceId, input.workspaceId), inArray(people.id, assignments.map((a) => a.personId))))
703
- .orderBy(asc(people.displayName));
704
- const primaryHere = new Set(assignments.filter((a) => a.isPrimary).map((a) => a.personId));
813
+ .orderBy(asc(people.displayName), asc(people.id))
814
+ .limit(input.limit + 1), input.limit, (r) => [r.person.displayName, r.person.id]);
705
815
  return {
706
816
  items: rows.map((r) => ({
707
- ...PeopleService.toPerson(r),
708
- isPrimaryHere: primaryHere.has(r.id),
817
+ ...PeopleService.toPerson(r.person),
818
+ // Migration 0001 allows one open assignment per person per office, so the join
819
+ // cannot produce a person twice and this flag cannot disagree with itself.
820
+ isPrimaryHere: r.isPrimary,
709
821
  })),
710
- nextCursor: null,
711
- total: rows.length,
822
+ nextCursor,
823
+ total: total?.n ?? 0,
712
824
  };
713
825
  })),
714
826
  assign: scoped.offices.assign
@@ -1444,13 +1556,16 @@ export function implement_(kernel) {
1444
1556
  const where = [eq(periods.workspaceId, input.workspaceId)];
1445
1557
  if (input.kind)
1446
1558
  where.push(eq(periods.kind, input.kind));
1447
- const rows = await tx
1559
+ const cursor = decodeCursor(input.cursor);
1560
+ if (cursor)
1561
+ where.push(after(periods.startsOn, periods.id, 'desc', cursor));
1562
+ const { items, nextCursor } = paginate(await tx
1448
1563
  .select()
1449
1564
  .from(periods)
1450
1565
  .where(and(...where))
1451
- .orderBy(desc(periods.startsOn))
1452
- .limit(input.limit);
1453
- return { items: rows.map(toPeriod), nextCursor: null };
1566
+ .orderBy(desc(periods.startsOn), desc(periods.id))
1567
+ .limit(input.limit + 1), input.limit, (r) => [r.startsOn, r.id]);
1568
+ return { items: items.map(toPeriod), nextCursor };
1454
1569
  })),
1455
1570
  create: scoped.periods.create
1456
1571
  .use(cap('periods'))
@@ -1616,13 +1731,21 @@ export function implement_(kernel) {
1616
1731
  ];
1617
1732
  if (!input.includeVoided)
1618
1733
  where.push(isNull(punches.voidedByPunchId));
1619
- const rows = await tx
1620
- .select()
1734
+ const cursor = decodeCursor(input.cursor);
1735
+ if (cursor)
1736
+ where.push(after(punches.at, punches.id, 'asc', cursor));
1737
+ const { items, nextCursor } = paginate(
1738
+ // `atText`, for the reason in `paginate`. Every insert here supplies a JS `Date`, so
1739
+ // nothing sub-millisecond is stored today and the bug is latent rather than live —
1740
+ // but the column carries `.defaultNow()`, so one insert that omits `at` would start
1741
+ // dropping punches off the end of a page with nothing to show for it.
1742
+ await tx
1743
+ .select({ ...getTableColumns(punches), atText: sql `${punches.at}::text` })
1621
1744
  .from(punches)
1622
1745
  .where(and(...where))
1623
- .orderBy(asc(punches.at))
1624
- .limit(input.limit);
1625
- return { items: rows.map(toPunch), nextCursor: null };
1746
+ .orderBy(asc(punches.at), asc(punches.id))
1747
+ .limit(input.limit + 1), input.limit, (r) => [r.atText, r.id]);
1748
+ return { items: items.map(toPunch), nextCursor };
1626
1749
  })),
1627
1750
  void: scoped.attendance.punches.void
1628
1751
  .use(cap('attendance'))
@@ -1669,13 +1792,16 @@ export function implement_(kernel) {
1669
1792
  const personId = await personFor(tx, input.workspaceId, context, input.personId);
1670
1793
  where.push(eq(attendanceDays.personId, personId));
1671
1794
  }
1672
- const rows = await tx
1795
+ const cursor = decodeCursor(input.cursor);
1796
+ if (cursor)
1797
+ where.push(after(attendanceDays.businessDate, attendanceDays.id, 'asc', cursor));
1798
+ const { items, nextCursor } = paginate(await tx
1673
1799
  .select()
1674
1800
  .from(attendanceDays)
1675
1801
  .where(and(...where))
1676
- .orderBy(asc(attendanceDays.businessDate))
1677
- .limit(input.limit);
1678
- return { items: rows.map(toAttendanceDay), nextCursor: null };
1802
+ .orderBy(asc(attendanceDays.businessDate), asc(attendanceDays.id))
1803
+ .limit(input.limit + 1), input.limit, (r) => [r.businessDate, r.id]);
1804
+ return { items: items.map(toAttendanceDay), nextCursor };
1679
1805
  })),
1680
1806
  recompute: scoped.attendance.days.recompute
1681
1807
  .use(cap('attendance'))
@@ -1813,13 +1939,16 @@ export function implement_(kernel) {
1813
1939
  ];
1814
1940
  if (input.status?.length)
1815
1941
  where.push(inArray(regularizations.status, input.status));
1816
- const rows = await tx
1942
+ const cursor = decodeCursor(input.cursor);
1943
+ if (cursor)
1944
+ where.push(after(regularizations.businessDate, regularizations.id, 'desc', cursor));
1945
+ const { items, nextCursor } = paginate(await tx
1817
1946
  .select()
1818
1947
  .from(regularizations)
1819
1948
  .where(and(...where))
1820
- .orderBy(desc(regularizations.businessDate))
1821
- .limit(input.limit);
1822
- return { items: rows.map(toRegularization), nextCursor: null };
1949
+ .orderBy(desc(regularizations.businessDate), desc(regularizations.id))
1950
+ .limit(input.limit + 1), input.limit, (r) => [r.businessDate, r.id]);
1951
+ return { items: items.map(toRegularization), nextCursor };
1823
1952
  })),
1824
1953
  request: scoped.attendance.regularizations.request
1825
1954
  .use(cap('attendance'))
@@ -1971,13 +2100,19 @@ export function implement_(kernel) {
1971
2100
  where.push(eq(leaveLedger.leaveTypeId, input.leaveTypeId));
1972
2101
  if (input.periodYear)
1973
2102
  where.push(eq(leaveLedger.periodYear, input.periodYear));
1974
- const rows = await tx
2103
+ const cursor = decodeCursor(input.cursor);
2104
+ if (cursor)
2105
+ where.push(after(leaveLedger.effectiveOn, leaveLedger.id, 'desc', cursor));
2106
+ // The second sort key is the id rather than `created_at`, and means the same thing:
2107
+ // ids here are uuidv7, so they already run in creation order — and unlike `created_at`
2108
+ // no two rows can share one, which is what makes the cursor land in exactly one place.
2109
+ const { items, nextCursor } = paginate(await tx
1975
2110
  .select()
1976
2111
  .from(leaveLedger)
1977
2112
  .where(and(...where))
1978
- .orderBy(desc(leaveLedger.effectiveOn), desc(leaveLedger.createdAt))
1979
- .limit(input.limit);
1980
- return { items: rows.map(toLedgerEntry), nextCursor: null };
2113
+ .orderBy(desc(leaveLedger.effectiveOn), desc(leaveLedger.id))
2114
+ .limit(input.limit + 1), input.limit, (r) => [r.effectiveOn, r.id]);
2115
+ return { items: items.map(toLedgerEntry), nextCursor };
1981
2116
  })),
1982
2117
  },
1983
2118
  adjust: scoped.leave.adjust
@@ -2013,11 +2148,34 @@ export function implement_(kernel) {
2013
2148
  .use(requires('hr.leave.view'))
2014
2149
  .handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
2015
2150
  const where = [eq(leaveRequests.workspaceId, input.workspaceId)];
2016
- if (input.personId)
2151
+ // An office is other people's absences, so asking for one costs the same permission
2152
+ // the team calendar costs. `hr.leave.view` alone must not become a way of reading the
2153
+ // whole company — which is exactly what a filter the server ignores had made of it.
2154
+ if (input.officeId) {
2155
+ await kernel.authz.require(context.principal, 'hr.leave.view_team', {
2156
+ kind: 'workspace',
2157
+ id: input.workspaceId,
2158
+ workspaceId: input.workspaceId,
2159
+ });
2160
+ const here = await tx
2161
+ .select({ personId: officeAssignments.personId })
2162
+ .from(officeAssignments)
2163
+ .where(and(eq(officeAssignments.workspaceId, input.workspaceId), eq(officeAssignments.officeId, input.officeId), isNull(officeAssignments.effectiveTo)));
2164
+ // An empty office matches nobody, not everybody.
2165
+ where.push(here.length
2166
+ ? inArray(leaveRequests.personId, here.map((h) => h.personId))
2167
+ : sql `false`);
2168
+ }
2169
+ if (input.personId) {
2170
+ // Naming somebody else is the same act as naming their office, and was the one way
2171
+ // through this handler that cost nothing: `hr.leave.view` and a person id read
2172
+ // anybody's absences. `personFor` is the rule everywhere else in this module.
2173
+ await personFor(tx, input.workspaceId, context, input.personId);
2017
2174
  where.push(eq(leaveRequests.personId, input.personId));
2018
- else if (!context.principal.instanceAdmin) {
2019
- // Without an explicit person, this is "my requests". Seeing everybody's by default
2020
- // would leak the whole company's absences to any member with hr.leave.view.
2175
+ }
2176
+ else if (!input.officeId && !context.principal.instanceAdmin) {
2177
+ // Without an explicit person or office, this is "my requests". Seeing everybody's by
2178
+ // default would leak the whole company's absences to any member with hr.leave.view.
2021
2179
  const me = await svc.byUserId(tx, input.workspaceId, context.principal.userId ?? '');
2022
2180
  where.push(me ? eq(leaveRequests.personId, me.id) : sql `false`);
2023
2181
  }
@@ -2027,13 +2185,16 @@ export function implement_(kernel) {
2027
2185
  where.push(gte(leaveRequests.endsOn, input.from));
2028
2186
  if (input.to)
2029
2187
  where.push(lte(leaveRequests.startsOn, input.to));
2030
- const rows = await tx
2188
+ const cursor = decodeCursor(input.cursor);
2189
+ if (cursor)
2190
+ where.push(after(leaveRequests.startsOn, leaveRequests.id, 'desc', cursor));
2191
+ const { items, nextCursor } = paginate(await tx
2031
2192
  .select()
2032
2193
  .from(leaveRequests)
2033
2194
  .where(and(...where))
2034
- .orderBy(desc(leaveRequests.startsOn))
2035
- .limit(input.limit);
2036
- return { items: rows.map(toLeaveRequest), nextCursor: null };
2195
+ .orderBy(desc(leaveRequests.startsOn), desc(leaveRequests.id))
2196
+ .limit(input.limit + 1), input.limit, (r) => [r.startsOn, r.id]);
2197
+ return { items: items.map(toLeaveRequest), nextCursor };
2037
2198
  })),
2038
2199
  get: scoped.leave.requests.get
2039
2200
  .use(cap('leave'))
@@ -2046,11 +2207,31 @@ export function implement_(kernel) {
2046
2207
  const personId = await personFor(tx, input.workspaceId, context, input.personId);
2047
2208
  return simulate(tx, input.workspaceId, personId, input);
2048
2209
  })),
2210
+ /**
2211
+ * File a request.
2212
+ *
2213
+ * `idempotencyKey` is the contract's promise that a retried submission is safe, and the
2214
+ * server used to store the key without ever reading it. So a retry filed a second request,
2215
+ * or died on whichever unique index it reached first — the exploded days for a counted
2216
+ * absence, the key itself otherwise. Neither is what the caller was promised.
2217
+ *
2218
+ * `hr_leave_requests_idem_uq` is what there is to lean on: it already refuses a second row
2219
+ * for a key, so honouring the promise is a read before the insert and an answer for the
2220
+ * loser of the race.
2221
+ */
2049
2222
  create: scoped.leave.requests.create
2050
2223
  .use(cap('leave'))
2051
2224
  .use(requires('hr.leave.request'))
2052
2225
  .handler(async ({ input, context }) => {
2053
- const result = await db.withWorkspace(input.workspaceId, async (tx) => {
2226
+ const key = input.idempotencyKey;
2227
+ // Held as a promise rather than awaited here, so that the duplicate recovery below is
2228
+ // one step at the end instead of ninety lines wrapped in a `try`.
2229
+ const filing = db.withWorkspace(input.workspaceId, async (tx) => {
2230
+ // Before anything is locked or simulated: the same key is the same submission, and
2231
+ // answering it with the request it already filed is what "safe to retry" means.
2232
+ const already = key ? await byIdempotencyKey(tx, input.workspaceId, key) : undefined;
2233
+ if (already)
2234
+ return { request: already, personId: already.personId, replay: true };
2054
2235
  const personId = await personFor(tx, input.workspaceId, context, input.personId);
2055
2236
  // Everything that spends balance takes the cursor lock first, inside this
2056
2237
  // transaction. Two overlapping requests for the last day cannot both read "enough".
@@ -2114,8 +2295,26 @@ export function implement_(kernel) {
2114
2295
  if (raised.autoApproved)
2115
2296
  await applyApproval(tx, input.workspaceId, request.id, context.principal.userId ?? null);
2116
2297
  const [fresh] = await tx.select().from(leaveRequests).where(eq(leaveRequests.id, request.id));
2117
- return { request: fresh, approvers: raised.firstStepApprovers, personId };
2298
+ return { request: fresh, personId, replay: false };
2299
+ });
2300
+ const result = await filing.catch(async (err) => {
2301
+ // The other half of a double submit: two clicks a browser sends before the first has
2302
+ // answered both pass the read above, both reach the insert, and the unique index
2303
+ // refuses one. The loser lost a race it was never meant to enter, so it is answered
2304
+ // with the request that won rather than with a constraint error. The re-read needs a
2305
+ // new transaction — the failed one is aborted and will not answer another query.
2306
+ if (!key || !isUniqueViolation(err, 'hr_leave_requests_idem_uq'))
2307
+ throw err;
2308
+ const won = await db.withWorkspace(input.workspaceId, (tx) => byIdempotencyKey(tx, input.workspaceId, key));
2309
+ if (!won)
2310
+ throw err;
2311
+ return { request: won, personId: won.personId, replay: true };
2118
2312
  });
2313
+ // A replay files nothing, so it announces nothing. Emitting `leaveRequested` again would
2314
+ // put a second card in an approver's inbox for one request — the outcome the key exists
2315
+ // to prevent, arriving by a different route.
2316
+ if (result.replay)
2317
+ return toLeaveRequest(result.request);
2119
2318
  await kernel.emit(hrEvents.leaveRequested, {
2120
2319
  requestId: result.request.id,
2121
2320
  workspaceId: input.workspaceId,
@@ -2236,6 +2435,13 @@ export function implement_(kernel) {
2236
2435
  /**
2237
2436
  * Everything waiting on the caller. No permission: an inbox of what *you* must decide is
2238
2437
  * yours by definition, and the engine only lists steps you are named on.
2438
+ *
2439
+ * The one paged list here that still answers `nextCursor: null`, and the only one that cannot
2440
+ * be fixed from this file: the page is cut inside `ApprovalService.inboxFor`, which takes a
2441
+ * limit and no cursor. Honouring one means widening that signature and pushing
2442
+ * `after(approvalRequests.requestedAt, approvalRequests.id, 'desc', …)` into its final query —
2443
+ * ordering it by `(requested_at, id)` on the way, so the cursor has something unique to land
2444
+ * on. Filtering the rows it returns would not do: it has already truncated them.
2239
2445
  */
2240
2446
  inbox: scoped.approvals.inbox.handler(({ input, context }) => db.withWorkspace(input.workspaceId, async (tx) => {
2241
2447
  const me = await svc.byUserId(tx, input.workspaceId, context.principal.userId ?? '');
@@ -2774,6 +2980,15 @@ export function implement_(kernel) {
2774
2980
  });
2775
2981
  return personId;
2776
2982
  }
2983
+ /** The request a retry is a retry *of*, or undefined the first time a key is seen. */
2984
+ async function byIdempotencyKey(tx, workspaceId, key) {
2985
+ const [row] = await tx
2986
+ .select()
2987
+ .from(leaveRequests)
2988
+ .where(and(eq(leaveRequests.workspaceId, workspaceId), eq(leaveRequests.idempotencyKey, key)))
2989
+ .limit(1);
2990
+ return row;
2991
+ }
2777
2992
  async function loadRequest(tx, workspaceId, requestId) {
2778
2993
  const [row] = await tx
2779
2994
  .select()