@substrat-run/engine-absence 0.4.7 → 0.5.1

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.
@@ -0,0 +1,443 @@
1
+ /**
2
+ * engine-absence' declared operation surface (#707/#865/#891/#896).
3
+ *
4
+ * ## Why this file exists now
5
+ *
6
+ * Six of this engine's checks narrow to an entity, and until #896 they were
7
+ * **undeclarable rather than merely undeclared** — the two halves of that being
8
+ * separate problems:
9
+ *
10
+ * 1. There was no declaration to put them in. `index.ts` carried a map of
11
+ * handlers, so `ctx.check(PERM.read, ref)` and `ctx.check(PERM.read)` looked
12
+ * the same to every tool in the repo, and `entityCheckConformanceSuite`
13
+ * derives its behavioural pair from `permission` (#891).
14
+ * 2. The format could not hold them. A narrowed check named `entity: '<a type
15
+ * from a declared registry>'`, and this engine narrows to a subject whose type
16
+ * is the VERTICAL's — Meridian's `employee`, which appears in no registry
17
+ * absence can see. #890's `entityFrom` did not reach it either: it changes
18
+ * where the type name comes from, not that it has to be a name someone
19
+ * declared. `refFrom` names the field carrying the whole ref, so the type
20
+ * travels with it and needs no name here at all (#896).
21
+ *
22
+ * What is deliberately NOT here is `http`: an engine owns no URL shape. Meridian
23
+ * composes this engine by CALL — importing `requestAbsence`, `balanceAsOf` and
24
+ * the rest into its own operations — so these bindings are the engine's default
25
+ * surface rather than the path any vertical serves.
26
+ *
27
+ * ## Four narrow, two say node, and the two are the interesting ones
28
+ *
29
+ * `request`, `balance`, `availability` and `list-entries` declare `refFrom` and
30
+ * the kit drives all four. The other two checks are real and are **not** declared
31
+ * as narrowed, each for a reason worth stating rather than leaving to be
32
+ * inferred:
33
+ *
34
+ * - **`cancel` has two authorities and the narrowed one is resolved.** An
35
+ * approver cancels anything cancellable (node); a subject may withdraw their
36
+ * own still-`requested` row through the same grant they requested with — but
37
+ * that ref is read off the stored request, not the input, so there is no field
38
+ * to name. Declaring the node key alone would say a subject needs
39
+ * `absence:approve` to withdraw, which is false. It declares the gate it opens
40
+ * with and states the second path here.
41
+ * - **`list-requests` narrows only when the caller supplies a subject.** An
42
+ * `optional` field behind `refFrom` would claim a narrowing that a caller
43
+ * omitting it never gets — the unsafe direction for a review artifact to be
44
+ * wrong in, and the same call Meridian's `hr/list-leave-types` and Shop's
45
+ * `catalog` made (#892).
46
+ *
47
+ * ## Paging, and why no `over`
48
+ *
49
+ * Three reads answer lists, and #811 refuses a bare array. None is
50
+ * kernel-composed: the ledger and the request book are ROWS this engine owns, not
51
+ * registry entities — `absenceEntities` declares exactly one, `leave-type` — so
52
+ * `paged.over` has nothing to name for two of the three, and the third answers a
53
+ * projection (`active` as a boolean) rather than the stored row. So all three
54
+ * page over their own fold, and **this change provisions no list index and adds
55
+ * no migration.**
56
+ */
57
+ export declare const ABSENCE_PERMISSIONS: readonly ['absence:read', 'absence:request', 'absence:approve', 'absence:configure'];
58
+ export declare const absenceOperations: {
59
+ readonly 'absence/configure-leave-type': {
60
+ readonly summary: "Create or update a leave type and its balance floor";
61
+ readonly permission: "absence:configure";
62
+ readonly input: import("zod").ZodObject<{
63
+ key: import("zod").ZodString;
64
+ floor: import("zod").ZodOptional<import("zod").ZodString>;
65
+ active: import("zod").ZodOptional<import("zod").ZodBoolean>;
66
+ }, import("zod/v4/core").$strip>;
67
+ readonly output: import("zod").ZodObject<{
68
+ key: import("zod").ZodString;
69
+ floor: import("zod").ZodString;
70
+ active: import("zod").ZodBoolean;
71
+ createdAt: import("zod").ZodString;
72
+ }, import("zod/v4/core").$strip>;
73
+ };
74
+ readonly 'absence/list-leave-types': {
75
+ readonly summary: "The configured leave types, by key";
76
+ readonly permission: "absence:read";
77
+ readonly output: import("zod").ZodObject<{
78
+ key: import("zod").ZodString;
79
+ floor: import("zod").ZodString;
80
+ active: import("zod").ZodBoolean;
81
+ createdAt: import("zod").ZodString;
82
+ }, import("zod/v4/core").$strip>;
83
+ readonly paged: {
84
+ readonly sortKey: "key";
85
+ };
86
+ };
87
+ readonly 'absence/record-entry': {
88
+ readonly summary: "Write an accrual, correction or carryover to the ledger";
89
+ readonly permission: "absence:configure";
90
+ readonly input: import("zod").ZodObject<{
91
+ subject: import("zod").ZodObject<{
92
+ ref: import("zod").ZodObject<{
93
+ entityType: import("zod").ZodString;
94
+ entityId: import("zod").ZodString;
95
+ }, import("zod/v4/core").$strip>;
96
+ dataSubjectId: import("zod/v4/core").$ZodBranded<import("zod").ZodString, "DataSubjectId", "out">;
97
+ }, import("zod/v4/core").$strip>;
98
+ leaveTypeKey: import("zod").ZodString;
99
+ entryKind: import("zod").ZodEnum<{
100
+ accrual: "accrual";
101
+ carryover: "carryover";
102
+ correction: "correction";
103
+ }>;
104
+ delta: import("zod").ZodString;
105
+ effectiveDate: import("zod").ZodString;
106
+ note: import("zod").ZodOptional<import("zod").ZodString>;
107
+ }, import("zod/v4/core").$strip>;
108
+ readonly output: import("zod").ZodObject<{
109
+ id: import("zod").ZodString;
110
+ subject: import("zod").ZodObject<{
111
+ entityType: import("zod").ZodString;
112
+ entityId: import("zod").ZodString;
113
+ }, import("zod/v4/core").$strip>;
114
+ leaveTypeKey: import("zod").ZodString;
115
+ entryKind: import("zod").ZodEnum<{
116
+ accrual: "accrual";
117
+ booking: "booking";
118
+ carryover: "carryover";
119
+ correction: "correction";
120
+ reversal: "reversal";
121
+ }>;
122
+ delta: import("zod").ZodString;
123
+ effectiveDate: import("zod").ZodString;
124
+ requestId: import("zod").ZodNullable<import("zod").ZodString>;
125
+ note: import("zod").ZodNullable<import("zod").ZodString>;
126
+ createdBy: import("zod").ZodString;
127
+ createdAt: import("zod").ZodString;
128
+ }, import("zod/v4/core").$strip>;
129
+ };
130
+ readonly 'absence/request': {
131
+ readonly summary: "Request absence for a subject";
132
+ /**
133
+ * The shape #896 exists for. A subject requests for THEMSELVES through a
134
+ * grant on their own ref while holding no role at all, so a node check here
135
+ * would let anyone holding `absence:request` anywhere in the scope file
136
+ * absence against anyone — with every test still green, because the engine's
137
+ * own suite grants scope-wide.
138
+ *
139
+ * `subject.ref` reaches one level in: the erasure key travels beside the ref
140
+ * (`{ ref, dataSubjectId }`) and only the ref is checked.
141
+ */
142
+ readonly permission: {
143
+ readonly key: "absence:request";
144
+ readonly refFrom: "subject.ref";
145
+ };
146
+ readonly input: import("zod").ZodObject<{
147
+ subject: import("zod").ZodObject<{
148
+ ref: import("zod").ZodObject<{
149
+ entityType: import("zod").ZodString;
150
+ entityId: import("zod").ZodString;
151
+ }, import("zod/v4/core").$strip>;
152
+ dataSubjectId: import("zod/v4/core").$ZodBranded<import("zod").ZodString, "DataSubjectId", "out">;
153
+ }, import("zod/v4/core").$strip>;
154
+ leaveTypeKey: import("zod").ZodString;
155
+ startDate: import("zod").ZodString;
156
+ endDate: import("zod").ZodString;
157
+ days: import("zod").ZodString;
158
+ note: import("zod").ZodOptional<import("zod").ZodString>;
159
+ }, import("zod/v4/core").$strip>;
160
+ readonly output: import("zod").ZodObject<{
161
+ id: import("zod").ZodString;
162
+ subject: import("zod").ZodObject<{
163
+ entityType: import("zod").ZodString;
164
+ entityId: import("zod").ZodString;
165
+ }, import("zod/v4/core").$strip>;
166
+ leaveTypeKey: import("zod").ZodString;
167
+ startDate: import("zod").ZodString;
168
+ endDate: import("zod").ZodString;
169
+ days: import("zod").ZodString;
170
+ status: import("zod").ZodEnum<{
171
+ approved: "approved";
172
+ cancelled: "cancelled";
173
+ rejected: "rejected";
174
+ requested: "requested";
175
+ }>;
176
+ note: import("zod").ZodNullable<import("zod").ZodString>;
177
+ decidedBy: import("zod").ZodNullable<import("zod").ZodString>;
178
+ decidedAt: import("zod").ZodNullable<import("zod").ZodString>;
179
+ createdBy: import("zod").ZodString;
180
+ createdAt: import("zod").ZodString;
181
+ }, import("zod/v4/core").$strip>;
182
+ };
183
+ readonly 'absence/decide': {
184
+ readonly summary: "Approve or reject a requested absence";
185
+ readonly permission: "absence:approve";
186
+ readonly input: import("zod").ZodObject<{
187
+ requestId: import("zod").ZodString;
188
+ decision: import("zod").ZodEnum<{
189
+ approve: "approve";
190
+ reject: "reject";
191
+ }>;
192
+ note: import("zod").ZodOptional<import("zod").ZodString>;
193
+ }, import("zod/v4/core").$strip>;
194
+ readonly output: import("zod").ZodObject<{
195
+ request: import("zod").ZodObject<{
196
+ id: import("zod").ZodString;
197
+ subject: import("zod").ZodObject<{
198
+ entityType: import("zod").ZodString;
199
+ entityId: import("zod").ZodString;
200
+ }, import("zod/v4/core").$strip>;
201
+ leaveTypeKey: import("zod").ZodString;
202
+ startDate: import("zod").ZodString;
203
+ endDate: import("zod").ZodString;
204
+ days: import("zod").ZodString;
205
+ status: import("zod").ZodEnum<{
206
+ approved: "approved";
207
+ cancelled: "cancelled";
208
+ rejected: "rejected";
209
+ requested: "requested";
210
+ }>;
211
+ note: import("zod").ZodNullable<import("zod").ZodString>;
212
+ decidedBy: import("zod").ZodNullable<import("zod").ZodString>;
213
+ decidedAt: import("zod").ZodNullable<import("zod").ZodString>;
214
+ createdBy: import("zod").ZodString;
215
+ createdAt: import("zod").ZodString;
216
+ }, import("zod/v4/core").$strip>;
217
+ booking: import("zod").ZodNullable<import("zod").ZodObject<{
218
+ id: import("zod").ZodString;
219
+ subject: import("zod").ZodObject<{
220
+ entityType: import("zod").ZodString;
221
+ entityId: import("zod").ZodString;
222
+ }, import("zod/v4/core").$strip>;
223
+ leaveTypeKey: import("zod").ZodString;
224
+ entryKind: import("zod").ZodEnum<{
225
+ accrual: "accrual";
226
+ booking: "booking";
227
+ carryover: "carryover";
228
+ correction: "correction";
229
+ reversal: "reversal";
230
+ }>;
231
+ delta: import("zod").ZodString;
232
+ effectiveDate: import("zod").ZodString;
233
+ requestId: import("zod").ZodNullable<import("zod").ZodString>;
234
+ note: import("zod").ZodNullable<import("zod").ZodString>;
235
+ createdBy: import("zod").ZodString;
236
+ createdAt: import("zod").ZodString;
237
+ }, import("zod/v4/core").$strip>>;
238
+ }, import("zod/v4/core").$strip>;
239
+ };
240
+ readonly 'absence/cancel': {
241
+ readonly summary: "Cancel an approved absence, or withdraw a requested one";
242
+ readonly permission: "absence:approve";
243
+ readonly input: import("zod").ZodObject<{
244
+ requestId: import("zod").ZodString;
245
+ reason: import("zod").ZodOptional<import("zod").ZodString>;
246
+ }, import("zod/v4/core").$strip>;
247
+ readonly output: import("zod").ZodObject<{
248
+ request: import("zod").ZodObject<{
249
+ id: import("zod").ZodString;
250
+ subject: import("zod").ZodObject<{
251
+ entityType: import("zod").ZodString;
252
+ entityId: import("zod").ZodString;
253
+ }, import("zod/v4/core").$strip>;
254
+ leaveTypeKey: import("zod").ZodString;
255
+ startDate: import("zod").ZodString;
256
+ endDate: import("zod").ZodString;
257
+ days: import("zod").ZodString;
258
+ status: import("zod").ZodEnum<{
259
+ approved: "approved";
260
+ cancelled: "cancelled";
261
+ rejected: "rejected";
262
+ requested: "requested";
263
+ }>;
264
+ note: import("zod").ZodNullable<import("zod").ZodString>;
265
+ decidedBy: import("zod").ZodNullable<import("zod").ZodString>;
266
+ decidedAt: import("zod").ZodNullable<import("zod").ZodString>;
267
+ createdBy: import("zod").ZodString;
268
+ createdAt: import("zod").ZodString;
269
+ }, import("zod/v4/core").$strip>;
270
+ reversal: import("zod").ZodNullable<import("zod").ZodObject<{
271
+ id: import("zod").ZodString;
272
+ subject: import("zod").ZodObject<{
273
+ entityType: import("zod").ZodString;
274
+ entityId: import("zod").ZodString;
275
+ }, import("zod/v4/core").$strip>;
276
+ leaveTypeKey: import("zod").ZodString;
277
+ entryKind: import("zod").ZodEnum<{
278
+ accrual: "accrual";
279
+ booking: "booking";
280
+ carryover: "carryover";
281
+ correction: "correction";
282
+ reversal: "reversal";
283
+ }>;
284
+ delta: import("zod").ZodString;
285
+ effectiveDate: import("zod").ZodString;
286
+ requestId: import("zod").ZodNullable<import("zod").ZodString>;
287
+ note: import("zod").ZodNullable<import("zod").ZodString>;
288
+ createdBy: import("zod").ZodString;
289
+ createdAt: import("zod").ZodString;
290
+ }, import("zod/v4/core").$strip>>;
291
+ }, import("zod/v4/core").$strip>;
292
+ };
293
+ readonly 'absence/expire-stale': {
294
+ readonly summary: "Expire requests left undecided past their window";
295
+ readonly permission: "absence:approve";
296
+ readonly output: import("zod").ZodObject<{
297
+ expired: import("zod").ZodNumber;
298
+ }, import("zod/v4/core").$strip>;
299
+ };
300
+ readonly 'absence/balance': {
301
+ readonly summary: "Balance for one subject and leave type, as of a date";
302
+ readonly permission: {
303
+ readonly key: "absence:read";
304
+ readonly refFrom: "subject";
305
+ };
306
+ readonly input: import("zod").ZodObject<{
307
+ subject: import("zod").ZodObject<{
308
+ entityType: import("zod").ZodString;
309
+ entityId: import("zod").ZodString;
310
+ }, import("zod/v4/core").$strip>;
311
+ leaveTypeKey: import("zod").ZodString;
312
+ asOf: import("zod").ZodOptional<import("zod").ZodString>;
313
+ }, import("zod/v4/core").$strip>;
314
+ readonly output: import("zod").ZodObject<{
315
+ balance: import("zod").ZodString;
316
+ }, import("zod/v4/core").$strip>;
317
+ };
318
+ readonly 'absence/availability': {
319
+ readonly summary: "Which calendar days an approved absence covers, for one subject";
320
+ readonly permission: {
321
+ readonly key: "absence:read";
322
+ readonly refFrom: "subject";
323
+ };
324
+ readonly input: import("zod").ZodObject<{
325
+ subject: import("zod").ZodObject<{
326
+ entityType: import("zod").ZodString;
327
+ entityId: import("zod").ZodString;
328
+ }, import("zod/v4/core").$strip>;
329
+ from: import("zod").ZodString;
330
+ to: import("zod").ZodString;
331
+ }, import("zod/v4/core").$strip>;
332
+ readonly output: import("zod").ZodObject<{
333
+ days: import("zod").ZodArray<import("zod").ZodObject<{
334
+ date: import("zod").ZodString;
335
+ leaveTypeKey: import("zod").ZodString;
336
+ requestId: import("zod").ZodString;
337
+ }, import("zod/v4/core").$strip>>;
338
+ requests: import("zod").ZodArray<import("zod").ZodObject<{
339
+ id: import("zod").ZodString;
340
+ subject: import("zod").ZodObject<{
341
+ entityType: import("zod").ZodString;
342
+ entityId: import("zod").ZodString;
343
+ }, import("zod/v4/core").$strip>;
344
+ leaveTypeKey: import("zod").ZodString;
345
+ startDate: import("zod").ZodString;
346
+ endDate: import("zod").ZodString;
347
+ days: import("zod").ZodString;
348
+ status: import("zod").ZodEnum<{
349
+ approved: "approved";
350
+ cancelled: "cancelled";
351
+ rejected: "rejected";
352
+ requested: "requested";
353
+ }>;
354
+ note: import("zod").ZodNullable<import("zod").ZodString>;
355
+ decidedBy: import("zod").ZodNullable<import("zod").ZodString>;
356
+ decidedAt: import("zod").ZodNullable<import("zod").ZodString>;
357
+ createdBy: import("zod").ZodString;
358
+ createdAt: import("zod").ZodString;
359
+ }, import("zod/v4/core").$strip>>;
360
+ }, import("zod/v4/core").$strip>;
361
+ };
362
+ readonly 'absence/list-requests': {
363
+ readonly summary: "Requests, optionally for one subject or one status";
364
+ readonly permission: "absence:read";
365
+ readonly input: import("zod").ZodObject<{
366
+ subject: import("zod").ZodOptional<import("zod").ZodObject<{
367
+ entityType: import("zod").ZodString;
368
+ entityId: import("zod").ZodString;
369
+ }, import("zod/v4/core").$strip>>;
370
+ status: import("zod").ZodOptional<import("zod").ZodEnum<{
371
+ approved: "approved";
372
+ cancelled: "cancelled";
373
+ rejected: "rejected";
374
+ requested: "requested";
375
+ }>>;
376
+ }, import("zod/v4/core").$strip>;
377
+ readonly inputOptional: true;
378
+ readonly output: import("zod").ZodObject<{
379
+ id: import("zod").ZodString;
380
+ subject: import("zod").ZodObject<{
381
+ entityType: import("zod").ZodString;
382
+ entityId: import("zod").ZodString;
383
+ }, import("zod/v4/core").$strip>;
384
+ leaveTypeKey: import("zod").ZodString;
385
+ startDate: import("zod").ZodString;
386
+ endDate: import("zod").ZodString;
387
+ days: import("zod").ZodString;
388
+ status: import("zod").ZodEnum<{
389
+ approved: "approved";
390
+ cancelled: "cancelled";
391
+ rejected: "rejected";
392
+ requested: "requested";
393
+ }>;
394
+ note: import("zod").ZodNullable<import("zod").ZodString>;
395
+ decidedBy: import("zod").ZodNullable<import("zod").ZodString>;
396
+ decidedAt: import("zod").ZodNullable<import("zod").ZodString>;
397
+ createdBy: import("zod").ZodString;
398
+ createdAt: import("zod").ZodString;
399
+ }, import("zod/v4/core").$strip>;
400
+ readonly paged: {
401
+ readonly sortKey: "id";
402
+ };
403
+ };
404
+ readonly 'absence/list-entries': {
405
+ readonly summary: "One subject's ledger entries, oldest first";
406
+ readonly permission: {
407
+ readonly key: "absence:read";
408
+ readonly refFrom: "subject";
409
+ };
410
+ readonly input: import("zod").ZodObject<{
411
+ subject: import("zod").ZodObject<{
412
+ entityType: import("zod").ZodString;
413
+ entityId: import("zod").ZodString;
414
+ }, import("zod/v4/core").$strip>;
415
+ leaveTypeKey: import("zod").ZodOptional<import("zod").ZodString>;
416
+ }, import("zod/v4/core").$strip>;
417
+ readonly output: import("zod").ZodObject<{
418
+ id: import("zod").ZodString;
419
+ subject: import("zod").ZodObject<{
420
+ entityType: import("zod").ZodString;
421
+ entityId: import("zod").ZodString;
422
+ }, import("zod/v4/core").$strip>;
423
+ leaveTypeKey: import("zod").ZodString;
424
+ entryKind: import("zod").ZodEnum<{
425
+ accrual: "accrual";
426
+ booking: "booking";
427
+ carryover: "carryover";
428
+ correction: "correction";
429
+ reversal: "reversal";
430
+ }>;
431
+ delta: import("zod").ZodString;
432
+ effectiveDate: import("zod").ZodString;
433
+ requestId: import("zod").ZodNullable<import("zod").ZodString>;
434
+ note: import("zod").ZodNullable<import("zod").ZodString>;
435
+ createdBy: import("zod").ZodString;
436
+ createdAt: import("zod").ZodString;
437
+ }, import("zod/v4/core").$strip>;
438
+ readonly paged: {
439
+ readonly sortKey: "effectiveDate";
440
+ };
441
+ };
442
+ };
443
+ //# sourceMappingURL=operations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../src/operations.ts"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,mBAAmB,YAC9B,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,CACX,CAAC;AAEX,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA6B1B;;;;;;;;;WASG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwEL,CAAC"}
@@ -0,0 +1,167 @@
1
+ import { defineOperations } from '@substrat-run/contracts';
2
+ import { absenceEntities } from './entities.js';
3
+ import { absenceEntry, absenceRequest, availabilityAnswer, availabilityInput, balanceAnswer, balanceInput, cancelAbsenceInput, cancelAnswer, configureLeaveTypeInput, decideAbsenceInput, decideAnswer, expiredAnswer, leaveType, listEntriesInput, listRequestsInput, recordEntryInput, requestAbsenceInput, } from './schemas.js';
4
+ /**
5
+ * engine-absence' declared operation surface (#707/#865/#891/#896).
6
+ *
7
+ * ## Why this file exists now
8
+ *
9
+ * Six of this engine's checks narrow to an entity, and until #896 they were
10
+ * **undeclarable rather than merely undeclared** — the two halves of that being
11
+ * separate problems:
12
+ *
13
+ * 1. There was no declaration to put them in. `index.ts` carried a map of
14
+ * handlers, so `ctx.check(PERM.read, ref)` and `ctx.check(PERM.read)` looked
15
+ * the same to every tool in the repo, and `entityCheckConformanceSuite`
16
+ * derives its behavioural pair from `permission` (#891).
17
+ * 2. The format could not hold them. A narrowed check named `entity: '<a type
18
+ * from a declared registry>'`, and this engine narrows to a subject whose type
19
+ * is the VERTICAL's — Meridian's `employee`, which appears in no registry
20
+ * absence can see. #890's `entityFrom` did not reach it either: it changes
21
+ * where the type name comes from, not that it has to be a name someone
22
+ * declared. `refFrom` names the field carrying the whole ref, so the type
23
+ * travels with it and needs no name here at all (#896).
24
+ *
25
+ * What is deliberately NOT here is `http`: an engine owns no URL shape. Meridian
26
+ * composes this engine by CALL — importing `requestAbsence`, `balanceAsOf` and
27
+ * the rest into its own operations — so these bindings are the engine's default
28
+ * surface rather than the path any vertical serves.
29
+ *
30
+ * ## Four narrow, two say node, and the two are the interesting ones
31
+ *
32
+ * `request`, `balance`, `availability` and `list-entries` declare `refFrom` and
33
+ * the kit drives all four. The other two checks are real and are **not** declared
34
+ * as narrowed, each for a reason worth stating rather than leaving to be
35
+ * inferred:
36
+ *
37
+ * - **`cancel` has two authorities and the narrowed one is resolved.** An
38
+ * approver cancels anything cancellable (node); a subject may withdraw their
39
+ * own still-`requested` row through the same grant they requested with — but
40
+ * that ref is read off the stored request, not the input, so there is no field
41
+ * to name. Declaring the node key alone would say a subject needs
42
+ * `absence:approve` to withdraw, which is false. It declares the gate it opens
43
+ * with and states the second path here.
44
+ * - **`list-requests` narrows only when the caller supplies a subject.** An
45
+ * `optional` field behind `refFrom` would claim a narrowing that a caller
46
+ * omitting it never gets — the unsafe direction for a review artifact to be
47
+ * wrong in, and the same call Meridian's `hr/list-leave-types` and Shop's
48
+ * `catalog` made (#892).
49
+ *
50
+ * ## Paging, and why no `over`
51
+ *
52
+ * Three reads answer lists, and #811 refuses a bare array. None is
53
+ * kernel-composed: the ledger and the request book are ROWS this engine owns, not
54
+ * registry entities — `absenceEntities` declares exactly one, `leave-type` — so
55
+ * `paged.over` has nothing to name for two of the three, and the third answers a
56
+ * projection (`active` as a boolean) rather than the stored row. So all three
57
+ * page over their own fold, and **this change provisions no list index and adds
58
+ * no migration.**
59
+ */
60
+ export const ABSENCE_PERMISSIONS = [
61
+ 'absence:read',
62
+ 'absence:request',
63
+ 'absence:approve',
64
+ 'absence:configure',
65
+ ];
66
+ export const absenceOperations = defineOperations(absenceEntities, ABSENCE_PERMISSIONS)({
67
+ 'absence/configure-leave-type': {
68
+ summary: 'Create or update a leave type and its balance floor',
69
+ permission: 'absence:configure',
70
+ input: configureLeaveTypeInput,
71
+ output: leaveType,
72
+ },
73
+ 'absence/list-leave-types': {
74
+ summary: 'The configured leave types, by key',
75
+ permission: 'absence:read',
76
+ output: leaveType,
77
+ // Handler-composed: what is answered is the PROJECTION of the row (`active`
78
+ // as a boolean), so the page is taken off the fold. `key` is the primary key,
79
+ // so it is both the order and a unique cursor.
80
+ paged: { sortKey: 'key' },
81
+ },
82
+ 'absence/record-entry': {
83
+ summary: 'Write an accrual, correction or carryover to the ledger',
84
+ // Node, deliberately: the one write that bypasses the request flow is an
85
+ // administrator's, over whichever subject needs correcting.
86
+ permission: 'absence:configure',
87
+ input: recordEntryInput,
88
+ output: absenceEntry,
89
+ },
90
+ 'absence/request': {
91
+ summary: 'Request absence for a subject',
92
+ /**
93
+ * The shape #896 exists for. A subject requests for THEMSELVES through a
94
+ * grant on their own ref while holding no role at all, so a node check here
95
+ * would let anyone holding `absence:request` anywhere in the scope file
96
+ * absence against anyone — with every test still green, because the engine's
97
+ * own suite grants scope-wide.
98
+ *
99
+ * `subject.ref` reaches one level in: the erasure key travels beside the ref
100
+ * (`{ ref, dataSubjectId }`) and only the ref is checked.
101
+ */
102
+ permission: { key: 'absence:request', refFrom: 'subject.ref' },
103
+ input: requestAbsenceInput,
104
+ output: absenceRequest,
105
+ },
106
+ 'absence/decide': {
107
+ summary: 'Approve or reject a requested absence',
108
+ // Node: deciding is the approver's authority over the whole scope. The
109
+ // 'booking' entry this mints is why approval is the only path into the
110
+ // ledger.
111
+ permission: 'absence:approve',
112
+ input: decideAbsenceInput,
113
+ output: decideAnswer,
114
+ },
115
+ 'absence/cancel': {
116
+ summary: 'Cancel an approved absence, or withdraw a requested one',
117
+ // The gate it OPENS with. The second authority — the subject withdrawing
118
+ // their own still-requested row, narrowed to a ref read off the stored
119
+ // request — is undeclarable here and is described in the header.
120
+ permission: 'absence:approve',
121
+ input: cancelAbsenceInput,
122
+ output: cancelAnswer,
123
+ },
124
+ 'absence/expire-stale': {
125
+ summary: 'Expire requests left undecided past their window',
126
+ // Node: a sweep acts on whatever has lapsed. Bound to the schedule this
127
+ // engine's own manifest declares (#383).
128
+ permission: 'absence:approve',
129
+ output: expiredAnswer,
130
+ },
131
+ 'absence/balance': {
132
+ summary: 'Balance for one subject and leave type, as of a date',
133
+ permission: { key: 'absence:read', refFrom: 'subject' },
134
+ input: balanceInput,
135
+ output: balanceAnswer,
136
+ },
137
+ 'absence/availability': {
138
+ summary: 'Which calendar days an approved absence covers, for one subject',
139
+ permission: { key: 'absence:read', refFrom: 'subject' },
140
+ input: availabilityInput,
141
+ output: availabilityAnswer,
142
+ },
143
+ 'absence/list-requests': {
144
+ summary: 'Requests, optionally for one subject or one status',
145
+ // Node — see the header. This is the conditional narrow, and declaring it
146
+ // would claim something a caller omitting `subject` does not pass.
147
+ permission: 'absence:read',
148
+ input: listRequestsInput,
149
+ inputOptional: true,
150
+ output: absenceRequest,
151
+ // Newest first, which is the order this read shipped with. The cursor is the
152
+ // id: it is a ULID, so it is unique and it descends with `created_at`.
153
+ paged: { sortKey: 'id' },
154
+ },
155
+ 'absence/list-entries': {
156
+ summary: "One subject's ledger entries, oldest first",
157
+ permission: { key: 'absence:read', refFrom: 'subject' },
158
+ input: listEntriesInput,
159
+ output: absenceEntry,
160
+ // `effectiveDate` is the order and is NOT unique — it is caller-supplied, so
161
+ // an accrual dated last year may be written today. The cursor is therefore
162
+ // the (effectiveDate, id) pair, the same pair the SQL orders by; `sortKey`
163
+ // names the field a reader sorts by, as Callout's timeline does over a rowid.
164
+ paged: { sortKey: 'effectiveDate' },
165
+ },
166
+ });
167
+ //# sourceMappingURL=operations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operations.js","sourceRoot":"","sources":["../src/operations.ts"],"names":[],"mappings":"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"}