iobroker.time-tracker 0.2.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/LICENSE +21 -0
- package/README.md +386 -0
- package/admin/i18n/de.json +34 -0
- package/admin/i18n/en.json +1 -0
- package/admin/i18n/es.json +34 -0
- package/admin/i18n/fr.json +34 -0
- package/admin/i18n/it.json +34 -0
- package/admin/i18n/nl.json +34 -0
- package/admin/i18n/pl.json +34 -0
- package/admin/i18n/pt.json +34 -0
- package/admin/i18n/ru.json +34 -0
- package/admin/i18n/uk.json +34 -0
- package/admin/i18n/zh-cn.json +34 -0
- package/admin/jsonConfig.json +266 -0
- package/admin/time-tracker.png +0 -0
- package/build/lib/adapter/automation.js +100 -0
- package/build/lib/adapter/automation.js.map +7 -0
- package/build/lib/adapter/commands.js +160 -0
- package/build/lib/adapter/commands.js.map +7 -0
- package/build/lib/adapter/messages.js +207 -0
- package/build/lib/adapter/messages.js.map +7 -0
- package/build/lib/adapter/presence.js +129 -0
- package/build/lib/adapter/presence.js.map +7 -0
- package/build/lib/adapter/states.js +330 -0
- package/build/lib/adapter/states.js.map +7 -0
- package/build/lib/adapter/triggers.js +92 -0
- package/build/lib/adapter/triggers.js.map +7 -0
- package/build/lib/db/database.js +96 -0
- package/build/lib/db/database.js.map +7 -0
- package/build/lib/db/migrations.js +603 -0
- package/build/lib/db/migrations.js.map +7 -0
- package/build/lib/db/repositories/absences.js +432 -0
- package/build/lib/db/repositories/absences.js.map +7 -0
- package/build/lib/db/repositories/audit.js +90 -0
- package/build/lib/db/repositories/audit.js.map +7 -0
- package/build/lib/db/repositories/automations.js +326 -0
- package/build/lib/db/repositories/automations.js.map +7 -0
- package/build/lib/db/repositories/entries.js +335 -0
- package/build/lib/db/repositories/entries.js.map +7 -0
- package/build/lib/db/repositories/holidays.js +156 -0
- package/build/lib/db/repositories/holidays.js.map +7 -0
- package/build/lib/db/repositories/payouts.js +221 -0
- package/build/lib/db/repositories/payouts.js.map +7 -0
- package/build/lib/db/repositories/rfid.js +264 -0
- package/build/lib/db/repositories/rfid.js.map +7 -0
- package/build/lib/db/repositories/rules.js +356 -0
- package/build/lib/db/repositories/rules.js.map +7 -0
- package/build/lib/db/repositories/settings.js +91 -0
- package/build/lib/db/repositories/settings.js.map +7 -0
- package/build/lib/db/repositories/terminals.js +243 -0
- package/build/lib/db/repositories/terminals.js.map +7 -0
- package/build/lib/db/repositories/triggers.js +240 -0
- package/build/lib/db/repositories/triggers.js.map +7 -0
- package/build/lib/db/repositories/users.js +491 -0
- package/build/lib/db/repositories/users.js.map +7 -0
- package/build/lib/db/seed.js +180 -0
- package/build/lib/db/seed.js.map +7 -0
- package/build/lib/domain/avatar.js +63 -0
- package/build/lib/domain/avatar.js.map +7 -0
- package/build/lib/domain/breaks.js +70 -0
- package/build/lib/domain/breaks.js.map +7 -0
- package/build/lib/domain/calculation.js +150 -0
- package/build/lib/domain/calculation.js.map +7 -0
- package/build/lib/domain/holidays.js +113 -0
- package/build/lib/domain/holidays.js.map +7 -0
- package/build/lib/domain/punch.js +99 -0
- package/build/lib/domain/punch.js.map +7 -0
- package/build/lib/domain/target.js +89 -0
- package/build/lib/domain/target.js.map +7 -0
- package/build/lib/errors.js +56 -0
- package/build/lib/errors.js.map +7 -0
- package/build/lib/reports/labels.js +358 -0
- package/build/lib/reports/labels.js.map +7 -0
- package/build/lib/reports/pdf.js +308 -0
- package/build/lib/reports/pdf.js.map +7 -0
- package/build/lib/reports/types.js +32 -0
- package/build/lib/reports/types.js.map +7 -0
- package/build/lib/reports/xls.js +211 -0
- package/build/lib/reports/xls.js.map +7 -0
- package/build/lib/services/aggregation.js +495 -0
- package/build/lib/services/aggregation.js.map +7 -0
- package/build/lib/services/auth.js +381 -0
- package/build/lib/services/auth.js.map +7 -0
- package/build/lib/services/backup.js +372 -0
- package/build/lib/services/backup.js.map +7 -0
- package/build/lib/services/closing.js +106 -0
- package/build/lib/services/closing.js.map +7 -0
- package/build/lib/services/sessionSecret.js +100 -0
- package/build/lib/services/sessionSecret.js.map +7 -0
- package/build/lib/services/sync.js +278 -0
- package/build/lib/services/sync.js.map +7 -0
- package/build/lib/util/time.js +125 -0
- package/build/lib/util/time.js.map +7 -0
- package/build/lib/web/api.js +2589 -0
- package/build/lib/web/api.js.map +7 -0
- package/build/lib/web/cookies.js +80 -0
- package/build/lib/web/cookies.js.map +7 -0
- package/build/lib/web/events.js +50 -0
- package/build/lib/web/events.js.map +7 -0
- package/build/lib/web/pin-guard.js +86 -0
- package/build/lib/web/pin-guard.js.map +7 -0
- package/build/lib/web/problem.js +129 -0
- package/build/lib/web/problem.js.map +7 -0
- package/build/lib/web/rate-limit.js +65 -0
- package/build/lib/web/rate-limit.js.map +7 -0
- package/build/lib/web/router.js +274 -0
- package/build/lib/web/router.js.map +7 -0
- package/build/lib/web/server.js +189 -0
- package/build/lib/web/server.js.map +7 -0
- package/build/lib/web/static.js +151 -0
- package/build/lib/web/static.js.map +7 -0
- package/build/lib/web/stream.js +135 -0
- package/build/lib/web/stream.js.map +7 -0
- package/build/main.js +986 -0
- package/build/main.js.map +7 -0
- package/io-package.json +221 -0
- package/package.json +131 -0
- package/src-pwa/src/i18n/de.json +359 -0
- package/src-pwa/src/i18n/en.json +359 -0
- package/src-pwa/src/i18n/es.json +359 -0
- package/src-pwa/src/i18n/fr.json +359 -0
- package/src-pwa/src/i18n/index.ts +96 -0
- package/src-pwa/src/i18n/it.json +359 -0
- package/src-pwa/src/i18n/nl.json +359 -0
- package/src-pwa/src/i18n/pl.json +359 -0
- package/src-pwa/src/i18n/pt.json +359 -0
- package/src-pwa/src/i18n/ru.json +359 -0
- package/src-pwa/src/i18n/uk.json +359 -0
- package/src-pwa/src/i18n/zh-cn.json +359 -0
- package/www/assets/index-QI3wrEvW.js +119 -0
- package/www/assets/index-QI3wrEvW.js.map +1 -0
- package/www/assets/workbox-window.prod.es5-Bd17z0YL.js +3 -0
- package/www/assets/workbox-window.prod.es5-Bd17z0YL.js.map +1 -0
- package/www/favicon.svg +16 -0
- package/www/icon-192.png +0 -0
- package/www/icon-512.png +0 -0
- package/www/index.html +38 -0
- package/www/manifest.webmanifest +1 -0
- package/www/person.png +0 -0
- package/www/sw.js +3 -0
- package/www/sw.js.map +1 -0
- package/www/workbox-9c191d2f.js +3 -0
- package/www/workbox-9c191d2f.js.map +1 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var entries_exports = {};
|
|
20
|
+
__export(entries_exports, {
|
|
21
|
+
RevisionConflictError: () => RevisionConflictError,
|
|
22
|
+
createEntriesRepository: () => createEntriesRepository,
|
|
23
|
+
mapEntryRow: () => mapEntryRow,
|
|
24
|
+
toPunchEntries: () => toPunchEntries
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(entries_exports);
|
|
27
|
+
var import_time = require("../../util/time");
|
|
28
|
+
var import_errors = require("../../errors");
|
|
29
|
+
var import_audit = require("./audit");
|
|
30
|
+
class RevisionConflictError extends Error {
|
|
31
|
+
/**
|
|
32
|
+
* Creates the error.
|
|
33
|
+
*
|
|
34
|
+
* @param entryId - id of the conflicting punch
|
|
35
|
+
* @param current - record as stored in the database
|
|
36
|
+
*/
|
|
37
|
+
constructor(entryId, current) {
|
|
38
|
+
super(`entry ${entryId} was modified by someone else (current revision ${current.revision})`);
|
|
39
|
+
this.entryId = entryId;
|
|
40
|
+
this.current = current;
|
|
41
|
+
this.name = "RevisionConflictError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function mapEntryRow(row) {
|
|
45
|
+
return {
|
|
46
|
+
id: row.id,
|
|
47
|
+
userId: row.user_id,
|
|
48
|
+
tsUtc: row.ts_utc,
|
|
49
|
+
clientTsUtc: row.client_ts_utc,
|
|
50
|
+
tsLocal: row.ts_local,
|
|
51
|
+
localDate: row.local_date,
|
|
52
|
+
direction: row.direction,
|
|
53
|
+
source: row.source,
|
|
54
|
+
idempotencyKey: row.idempotency_key,
|
|
55
|
+
syncState: row.sync_state,
|
|
56
|
+
revision: row.revision,
|
|
57
|
+
note: row.note
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function toPunchEntries(records) {
|
|
61
|
+
return records.map((record) => ({ id: record.id, tsUtc: record.tsUtc, syncState: record.syncState }));
|
|
62
|
+
}
|
|
63
|
+
const COLUMNS = `id, user_id, ts_utc, client_ts_utc, ts_local, local_date, direction, source,
|
|
64
|
+
idempotency_key, sync_state, revision, note`;
|
|
65
|
+
function createEntriesRepository(db) {
|
|
66
|
+
const selectById = db.prepare(`SELECT ${COLUMNS} FROM time_entries WHERE id = ?`);
|
|
67
|
+
const selectByIdempotency = db.prepare(
|
|
68
|
+
`SELECT ${COLUMNS} FROM time_entries WHERE user_id = ? AND idempotency_key = ?`
|
|
69
|
+
);
|
|
70
|
+
const selectByRange = db.prepare(
|
|
71
|
+
`SELECT ${COLUMNS} FROM time_entries
|
|
72
|
+
WHERE user_id = ? AND local_date >= ? AND local_date <= ?
|
|
73
|
+
ORDER BY ts_utc, id`
|
|
74
|
+
);
|
|
75
|
+
const selectByDate = db.prepare(
|
|
76
|
+
`SELECT ${COLUMNS} FROM time_entries WHERE user_id = ? AND local_date = ? ORDER BY ts_utc, id`
|
|
77
|
+
);
|
|
78
|
+
const selectBySyncState = db.prepare(
|
|
79
|
+
`SELECT ${COLUMNS} FROM time_entries WHERE user_id = ? AND sync_state = ? ORDER BY ts_utc DESC, id DESC`
|
|
80
|
+
);
|
|
81
|
+
const insertRow = db.prepare(
|
|
82
|
+
`INSERT INTO time_entries
|
|
83
|
+
(user_id, ts_utc, client_ts_utc, ts_local, local_date, direction, source, idempotency_key,
|
|
84
|
+
sync_state, revision, note, created_by, created_at, updated_at, updated_by)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)`
|
|
86
|
+
);
|
|
87
|
+
const updateRow = db.prepare(
|
|
88
|
+
`UPDATE time_entries
|
|
89
|
+
SET ts_utc = ?, ts_local = ?, local_date = ?, note = ?, source = ?, user_id = ?,
|
|
90
|
+
revision = revision + 1, updated_at = ?, updated_by = ?
|
|
91
|
+
WHERE id = ? AND revision = ?`
|
|
92
|
+
);
|
|
93
|
+
const deleteRow = db.prepare("DELETE FROM time_entries WHERE id = ?");
|
|
94
|
+
const updateSyncState = db.prepare(
|
|
95
|
+
"UPDATE time_entries SET sync_state = ?, updated_at = ?, updated_by = ? WHERE id = ?"
|
|
96
|
+
);
|
|
97
|
+
const read = (id) => {
|
|
98
|
+
const row = selectById.get(id);
|
|
99
|
+
return row ? mapEntryRow(row) : null;
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
insert(input) {
|
|
103
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
104
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
105
|
+
const idempotencyKey = (_b = input.idempotencyKey) != null ? _b : null;
|
|
106
|
+
if (idempotencyKey) {
|
|
107
|
+
const existing = selectByIdempotency.get(input.userId, idempotencyKey);
|
|
108
|
+
if (existing) {
|
|
109
|
+
return { entry: mapEntryRow(existing), created: false };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const record = {
|
|
113
|
+
userId: input.userId,
|
|
114
|
+
tsUtc: input.tsUtc,
|
|
115
|
+
clientTsUtc: (_c = input.clientTsUtc) != null ? _c : null,
|
|
116
|
+
tsLocal: (0, import_time.utcToWallTime)(input.tsUtc, input.timeZone),
|
|
117
|
+
localDate: (0, import_time.localDate)(input.tsUtc, input.timeZone),
|
|
118
|
+
direction: (_d = input.direction) != null ? _d : "auto",
|
|
119
|
+
source: (_e = input.source) != null ? _e : "web",
|
|
120
|
+
idempotencyKey,
|
|
121
|
+
syncState: (_f = input.syncState) != null ? _f : "synced",
|
|
122
|
+
note: (_g = input.note) != null ? _g : null
|
|
123
|
+
};
|
|
124
|
+
let entryId = 0;
|
|
125
|
+
const run = db.transaction(() => {
|
|
126
|
+
var _a2, _b2, _c2, _d2, _e2;
|
|
127
|
+
const result = insertRow.run(
|
|
128
|
+
record.userId,
|
|
129
|
+
record.tsUtc,
|
|
130
|
+
record.clientTsUtc,
|
|
131
|
+
record.tsLocal,
|
|
132
|
+
record.localDate,
|
|
133
|
+
record.direction,
|
|
134
|
+
record.source,
|
|
135
|
+
record.idempotencyKey,
|
|
136
|
+
record.syncState,
|
|
137
|
+
record.note,
|
|
138
|
+
(_a2 = input.actorId) != null ? _a2 : null,
|
|
139
|
+
now,
|
|
140
|
+
now,
|
|
141
|
+
(_b2 = input.actorId) != null ? _b2 : null
|
|
142
|
+
);
|
|
143
|
+
entryId = Number(result.lastInsertRowid);
|
|
144
|
+
(0, import_audit.writeTimeEntryAudit)(db, {
|
|
145
|
+
entryId,
|
|
146
|
+
userId: record.userId,
|
|
147
|
+
action: "create",
|
|
148
|
+
changes: {
|
|
149
|
+
tsUtc: { old: null, new: record.tsUtc },
|
|
150
|
+
source: { old: null, new: record.source }
|
|
151
|
+
},
|
|
152
|
+
newTsUtc: record.tsUtc,
|
|
153
|
+
revision: 1,
|
|
154
|
+
reason: (_c2 = input.reason) != null ? _c2 : null,
|
|
155
|
+
actorId: (_d2 = input.actorId) != null ? _d2 : record.userId,
|
|
156
|
+
actorIp: (_e2 = input.actorIp) != null ? _e2 : null,
|
|
157
|
+
atUtc: now
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
run();
|
|
161
|
+
const created = read(entryId);
|
|
162
|
+
if (!created) {
|
|
163
|
+
throw new Error(`entry ${entryId} disappeared right after creation`);
|
|
164
|
+
}
|
|
165
|
+
return { entry: created, created: true };
|
|
166
|
+
},
|
|
167
|
+
findById: read,
|
|
168
|
+
listByRange(userId, fromDate, toDate) {
|
|
169
|
+
const rows = selectByRange.all(userId, fromDate, toDate);
|
|
170
|
+
return rows.map(mapEntryRow);
|
|
171
|
+
},
|
|
172
|
+
listByDate(userId, localDate) {
|
|
173
|
+
const rows = selectByDate.all(userId, localDate);
|
|
174
|
+
return rows.map(mapEntryRow);
|
|
175
|
+
},
|
|
176
|
+
listBySyncState(userId, syncState) {
|
|
177
|
+
const rows = selectBySyncState.all(userId, syncState);
|
|
178
|
+
return rows.map(mapEntryRow);
|
|
179
|
+
},
|
|
180
|
+
findByIdempotencyKey(userId, idempotencyKey) {
|
|
181
|
+
const row = selectByIdempotency.get(userId, idempotencyKey);
|
|
182
|
+
return row ? mapEntryRow(row) : null;
|
|
183
|
+
},
|
|
184
|
+
setSyncState(input) {
|
|
185
|
+
var _a;
|
|
186
|
+
const current = read(input.id);
|
|
187
|
+
if (!current) {
|
|
188
|
+
throw new import_errors.NotFoundError(`entry ${input.id} not found`);
|
|
189
|
+
}
|
|
190
|
+
if (current.syncState === input.syncState) {
|
|
191
|
+
return current;
|
|
192
|
+
}
|
|
193
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
194
|
+
const run = db.transaction(() => {
|
|
195
|
+
var _a2, _b;
|
|
196
|
+
updateSyncState.run(input.syncState, now, input.actorId, input.id);
|
|
197
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
198
|
+
atUtc: now,
|
|
199
|
+
actorId: input.actorId,
|
|
200
|
+
action: "entry.sync_state",
|
|
201
|
+
entity: "time_entry",
|
|
202
|
+
entityId: input.id,
|
|
203
|
+
detail: {
|
|
204
|
+
changes: { syncState: { old: current.syncState, new: input.syncState } },
|
|
205
|
+
reason: (_a2 = input.reason) != null ? _a2 : null
|
|
206
|
+
},
|
|
207
|
+
ip: (_b = input.actorIp) != null ? _b : null
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
run();
|
|
211
|
+
const updated = read(input.id);
|
|
212
|
+
if (!updated) {
|
|
213
|
+
throw new Error(`entry ${input.id} disappeared right after the sync state change`);
|
|
214
|
+
}
|
|
215
|
+
return updated;
|
|
216
|
+
},
|
|
217
|
+
update(input) {
|
|
218
|
+
var _a, _b, _c, _d;
|
|
219
|
+
const current = read(input.id);
|
|
220
|
+
if (!current) {
|
|
221
|
+
throw new import_errors.NotFoundError(`entry ${input.id} not found`);
|
|
222
|
+
}
|
|
223
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
224
|
+
const nextTsUtc = (_b = input.patch.tsUtc) != null ? _b : current.tsUtc;
|
|
225
|
+
const nextUserId = (_c = input.patch.userId) != null ? _c : current.userId;
|
|
226
|
+
const nextNote = input.patch.note === void 0 ? current.note : input.patch.note;
|
|
227
|
+
const nextSource = (_d = input.patch.source) != null ? _d : current.source;
|
|
228
|
+
const changes = {};
|
|
229
|
+
if (nextTsUtc !== current.tsUtc) {
|
|
230
|
+
changes.tsUtc = { old: current.tsUtc, new: nextTsUtc };
|
|
231
|
+
}
|
|
232
|
+
if (nextUserId !== current.userId) {
|
|
233
|
+
changes.userId = { old: current.userId, new: nextUserId };
|
|
234
|
+
}
|
|
235
|
+
if (nextNote !== current.note) {
|
|
236
|
+
changes.note = { old: current.note, new: nextNote };
|
|
237
|
+
}
|
|
238
|
+
if (nextSource !== current.source) {
|
|
239
|
+
changes.source = { old: current.source, new: nextSource };
|
|
240
|
+
}
|
|
241
|
+
if (Object.keys(changes).length === 0) {
|
|
242
|
+
return current;
|
|
243
|
+
}
|
|
244
|
+
const run = db.transaction(() => {
|
|
245
|
+
var _a2, _b2, _c2, _d2, _e;
|
|
246
|
+
const result = updateRow.run(
|
|
247
|
+
nextTsUtc,
|
|
248
|
+
(0, import_time.utcToWallTime)(nextTsUtc, input.timeZone),
|
|
249
|
+
(0, import_time.localDate)(nextTsUtc, input.timeZone),
|
|
250
|
+
nextNote,
|
|
251
|
+
nextSource,
|
|
252
|
+
nextUserId,
|
|
253
|
+
now,
|
|
254
|
+
input.actorId,
|
|
255
|
+
input.id,
|
|
256
|
+
input.expectedRevision
|
|
257
|
+
);
|
|
258
|
+
if (result.changes === 0) {
|
|
259
|
+
throw new RevisionConflictError(input.id, (_a2 = read(input.id)) != null ? _a2 : current);
|
|
260
|
+
}
|
|
261
|
+
(0, import_audit.writeTimeEntryAudit)(db, {
|
|
262
|
+
entryId: input.id,
|
|
263
|
+
userId: current.userId,
|
|
264
|
+
action: "update",
|
|
265
|
+
changes,
|
|
266
|
+
oldTsUtc: current.tsUtc,
|
|
267
|
+
newTsUtc: nextTsUtc,
|
|
268
|
+
revision: input.expectedRevision + 1,
|
|
269
|
+
reason: (_b2 = input.reason) != null ? _b2 : null,
|
|
270
|
+
actorId: input.actorId,
|
|
271
|
+
actorIp: (_c2 = input.actorIp) != null ? _c2 : null,
|
|
272
|
+
atUtc: now
|
|
273
|
+
});
|
|
274
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
275
|
+
atUtc: now,
|
|
276
|
+
actorId: input.actorId,
|
|
277
|
+
action: "entry.update",
|
|
278
|
+
entity: "time_entry",
|
|
279
|
+
entityId: input.id,
|
|
280
|
+
detail: { changes, reason: (_d2 = input.reason) != null ? _d2 : null },
|
|
281
|
+
ip: (_e = input.actorIp) != null ? _e : null
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
run();
|
|
285
|
+
const updated = read(input.id);
|
|
286
|
+
if (!updated) {
|
|
287
|
+
throw new Error(`entry ${input.id} not found after update`);
|
|
288
|
+
}
|
|
289
|
+
return updated;
|
|
290
|
+
},
|
|
291
|
+
remove(input) {
|
|
292
|
+
var _a;
|
|
293
|
+
const current = read(input.id);
|
|
294
|
+
if (!current) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
298
|
+
const run = db.transaction(() => {
|
|
299
|
+
var _a2, _b, _c, _d;
|
|
300
|
+
deleteRow.run(input.id);
|
|
301
|
+
(0, import_audit.writeTimeEntryAudit)(db, {
|
|
302
|
+
entryId: input.id,
|
|
303
|
+
userId: current.userId,
|
|
304
|
+
action: "delete",
|
|
305
|
+
changes: { tsUtc: { old: current.tsUtc, new: null } },
|
|
306
|
+
oldTsUtc: current.tsUtc,
|
|
307
|
+
revision: current.revision + 1,
|
|
308
|
+
reason: (_a2 = input.reason) != null ? _a2 : null,
|
|
309
|
+
actorId: input.actorId,
|
|
310
|
+
actorIp: (_b = input.actorIp) != null ? _b : null,
|
|
311
|
+
atUtc: now
|
|
312
|
+
});
|
|
313
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
314
|
+
atUtc: now,
|
|
315
|
+
actorId: input.actorId,
|
|
316
|
+
action: "entry.delete",
|
|
317
|
+
entity: "time_entry",
|
|
318
|
+
entityId: input.id,
|
|
319
|
+
detail: { tsUtc: current.tsUtc, reason: (_c = input.reason) != null ? _c : null },
|
|
320
|
+
ip: (_d = input.actorIp) != null ? _d : null
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
run();
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
329
|
+
0 && (module.exports = {
|
|
330
|
+
RevisionConflictError,
|
|
331
|
+
createEntriesRepository,
|
|
332
|
+
mapEntryRow,
|
|
333
|
+
toPunchEntries
|
|
334
|
+
});
|
|
335
|
+
//# sourceMappingURL=entries.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/lib/db/repositories/entries.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Punch repository (`time_entries`): idempotent inserts, optimistic locking and audit trail.\n *\n * All writes go through a transaction together with their audit entry, so no punch change is ever\n * recorded without a trace.\n */\n\nimport type { Db } from \"../database\";\nimport type { PunchEntry } from \"../../domain/punch\";\nimport { utcToWallTime, localDate as resolveLocalDate } from \"../../util/time\";\nimport { NotFoundError } from \"../../errors\";\nimport { writeAuditLog, writeTimeEntryAudit, type FieldChange } from \"./audit\";\n\n/** Origin of a punch. */\nexport type EntrySource = \"web\" | \"nfc\" | \"terminal\" | \"api\" | \"admin\" | \"import\";\n/** Synchronisation state of a punch. */\nexport type EntrySyncState = \"synced\" | \"pending\" | \"conflict\";\n/** Stored direction hint (the pairing order stays authoritative). */\nexport type EntryDirection = \"in\" | \"out\" | \"auto\";\n\n/** A punch as stored in the database. */\nexport interface EntryRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Owner of the punch */\n\tuserId: number;\n\t/** Punch instant, UTC epoch seconds (authoritative) */\n\ttsUtc: number;\n\t/** Instant reported by the client (offline punches), informational only */\n\tclientTsUtc: number | null;\n\t/** Wall clock time of the user at the punch instant (derived cache) */\n\ttsLocal: number;\n\t/** Local date of the user at the punch instant (derived cache) */\n\tlocalDate: string;\n\t/** Direction hint for the UI (the pairing order stays authoritative) */\n\tdirection: EntryDirection;\n\t/** Origin of the punch */\n\tsource: EntrySource;\n\t/** Idempotency key of the client (offline queue, import) */\n\tidempotencyKey: string | null;\n\t/** Synchronisation state */\n\tsyncState: EntrySyncState;\n\t/** Revision counter for optimistic locking */\n\trevision: number;\n\t/** Free-form note of the employee or the administrator */\n\tnote: string | null;\n}\n\n/** Input for creating a punch. */\nexport interface CreateEntryInput {\n\t/** Owner of the punch */\n\tuserId: number;\n\t/** Punch instant, UTC epoch seconds */\n\ttsUtc: number;\n\t/** Client supplied instant (offline punches), informational only */\n\tclientTsUtc?: number | null;\n\t/** Time zone used to derive the local cache fields */\n\ttimeZone: string;\n\t/** Origin, default `web` */\n\tsource?: EntrySource;\n\t/** Direction hint, default `auto` */\n\tdirection?: EntryDirection;\n\t/** Idempotency key (offline queue, import) */\n\tidempotencyKey?: string | null;\n\t/** Synchronisation state, default `synced` */\n\tsyncState?: EntrySyncState;\n\t/** Free-form note */\n\tnote?: string | null;\n\t/** Reason of an administrative correction (audit trail) */\n\treason?: string | null;\n\t/** Actor creating the punch (for administrative creation) */\n\tactorId?: number | null;\n\t/** Actor IP address */\n\tactorIp?: string | null;\n\t/** Instant of creation, defaults to now */\n\tnow?: number;\n}\n\n/** Input for updating a punch (optimistic locking). */\nexport interface UpdateEntryInput {\n\t/** Id of the changed punch */\n\tid: number;\n\t/** Revision the caller has read */\n\texpectedRevision: number;\n\t/** Fields to change */\n\tpatch: {\n\t\ttsUtc?: number;\n\t\tnote?: string | null;\n\t\tsource?: EntrySource;\n\t\tuserId?: number;\n\t};\n\t/** Mandatory reason for administrative corrections */\n\treason?: string | null;\n\t/** Who performs the change */\n\tactorId: number;\n\t/** Client IP address of the actor */\n\tactorIp?: string | null;\n\t/** Time zone used to recompute the local cache fields */\n\ttimeZone: string;\n\t/** Instant of the change, defaults to now */\n\tnow?: number;\n}\n\n/** Thrown when a punch was modified by someone else in the meantime. */\nexport class RevisionConflictError extends Error {\n\t/**\n\t * Creates the error.\n\t *\n\t * @param entryId - id of the conflicting punch\n\t * @param current - record as stored in the database\n\t */\n\tconstructor(\n\t\tpublic readonly entryId: number,\n\t\tpublic readonly current: EntryRecord,\n\t) {\n\t\tsuper(`entry ${entryId} was modified by someone else (current revision ${current.revision})`);\n\t\tthis.name = \"RevisionConflictError\";\n\t}\n}\n\n/** Punch storage operations. */\nexport interface EntriesRepository {\n\t/** Creates a punch; repeated calls with the same idempotency key return the existing punch */\n\tinsert(input: CreateEntryInput): { entry: EntryRecord; created: boolean };\n\t/** Reads a punch by id */\n\tfindById(id: number): EntryRecord | null;\n\t/** Punches of one user within a local date range (inclusive) */\n\tlistByRange(userId: number, fromDate: string, toDate: string): EntryRecord[];\n\t/** Punches of one user on one local date */\n\tlistByDate(userId: number, localDate: string): EntryRecord[];\n\t/** Changes a punch; throws `RevisionConflictError` when the revision does not match */\n\tupdate(input: UpdateEntryInput): EntryRecord;\n\t/** Punches of one user with a given synchronisation state (newest first) */\n\tlistBySyncState(userId: number, syncState: EntrySyncState): EntryRecord[];\n\t/** Punch that was stored with the given idempotency key, `null` when unknown */\n\tfindByIdempotencyKey(userId: number, idempotencyKey: string): EntryRecord | null;\n\t/** Moves a punch into another synchronisation state (resolving offline conflicts) */\n\tsetSyncState(input: {\n\t\tid: number;\n\t\tsyncState: EntrySyncState;\n\t\tactorId: number;\n\t\treason?: string | null;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): EntryRecord;\n\t/** Deletes a punch and records the deletion in the audit trail */\n\tremove(input: {\n\t\tid: number;\n\t\tactorId: number;\n\t\treason?: string | null;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): boolean;\n}\n\ninterface EntryRow {\n\tid: number;\n\tuser_id: number;\n\tts_utc: number;\n\tclient_ts_utc: number | null;\n\tts_local: number;\n\tlocal_date: string;\n\tdirection: EntryDirection;\n\tsource: EntrySource;\n\tidempotency_key: string | null;\n\tsync_state: EntrySyncState;\n\trevision: number;\n\tnote: string | null;\n}\n\n/**\n * Maps a database row to a domain record.\n *\n * @param row - raw database row\n * @returns punch record\n */\nexport function mapEntryRow(row: EntryRow): EntryRecord {\n\treturn {\n\t\tid: row.id,\n\t\tuserId: row.user_id,\n\t\ttsUtc: row.ts_utc,\n\t\tclientTsUtc: row.client_ts_utc,\n\t\ttsLocal: row.ts_local,\n\t\tlocalDate: row.local_date,\n\t\tdirection: row.direction,\n\t\tsource: row.source,\n\t\tidempotencyKey: row.idempotency_key,\n\t\tsyncState: row.sync_state,\n\t\trevision: row.revision,\n\t\tnote: row.note,\n\t};\n}\n\n/**\n * Converts stored punches into pairing input.\n *\n * @param records - punch records\n * @returns minimal punch information for `buildDayPunches`\n */\nexport function toPunchEntries(records: EntryRecord[]): PunchEntry[] {\n\treturn records.map(record => ({ id: record.id, tsUtc: record.tsUtc, syncState: record.syncState }));\n}\n\nconst COLUMNS = `id, user_id, ts_utc, client_ts_utc, ts_local, local_date, direction, source,\n\tidempotency_key, sync_state, revision, note`;\n\n/**\n * Creates the punch repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createEntriesRepository(db: Db): EntriesRepository {\n\tconst selectById = db.prepare(`SELECT ${COLUMNS} FROM time_entries WHERE id = ?`);\n\tconst selectByIdempotency = db.prepare(\n\t\t`SELECT ${COLUMNS} FROM time_entries WHERE user_id = ? AND idempotency_key = ?`,\n\t);\n\tconst selectByRange = db.prepare(\n\t\t`SELECT ${COLUMNS} FROM time_entries\n\t\t WHERE user_id = ? AND local_date >= ? AND local_date <= ?\n\t\t ORDER BY ts_utc, id`,\n\t);\n\tconst selectByDate = db.prepare(\n\t\t`SELECT ${COLUMNS} FROM time_entries WHERE user_id = ? AND local_date = ? ORDER BY ts_utc, id`,\n\t);\n\tconst selectBySyncState = db.prepare(\n\t\t`SELECT ${COLUMNS} FROM time_entries WHERE user_id = ? AND sync_state = ? ORDER BY ts_utc DESC, id DESC`,\n\t);\n\tconst insertRow = db.prepare(\n\t\t`INSERT INTO time_entries\n\t\t (user_id, ts_utc, client_ts_utc, ts_local, local_date, direction, source, idempotency_key,\n\t\t sync_state, revision, note, created_by, created_at, updated_at, updated_by)\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)`,\n\t);\n\tconst updateRow = db.prepare(\n\t\t`UPDATE time_entries\n\t\t SET ts_utc = ?, ts_local = ?, local_date = ?, note = ?, source = ?, user_id = ?,\n\t\t revision = revision + 1, updated_at = ?, updated_by = ?\n\t\t WHERE id = ? AND revision = ?`,\n\t);\n\tconst deleteRow = db.prepare(\"DELETE FROM time_entries WHERE id = ?\");\n\tconst updateSyncState = db.prepare(\n\t\t\"UPDATE time_entries SET sync_state = ?, updated_at = ?, updated_by = ? WHERE id = ?\",\n\t);\n\n\tconst read = (id: number): EntryRecord | null => {\n\t\tconst row = selectById.get(id) as EntryRow | undefined;\n\t\treturn row ? mapEntryRow(row) : null;\n\t};\n\n\treturn {\n\t\tinsert(input: CreateEntryInput): { entry: EntryRecord; created: boolean } {\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst idempotencyKey = input.idempotencyKey ?? null;\n\n\t\t\tif (idempotencyKey) {\n\t\t\t\tconst existing = selectByIdempotency.get(input.userId, idempotencyKey) as EntryRow | undefined;\n\t\t\t\tif (existing) {\n\t\t\t\t\treturn { entry: mapEntryRow(existing), created: false };\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst record = {\n\t\t\t\tuserId: input.userId,\n\t\t\t\ttsUtc: input.tsUtc,\n\t\t\t\tclientTsUtc: input.clientTsUtc ?? null,\n\t\t\t\ttsLocal: utcToWallTime(input.tsUtc, input.timeZone),\n\t\t\t\tlocalDate: resolveLocalDate(input.tsUtc, input.timeZone),\n\t\t\t\tdirection: input.direction ?? \"auto\",\n\t\t\t\tsource: input.source ?? \"web\",\n\t\t\t\tidempotencyKey,\n\t\t\t\tsyncState: input.syncState ?? \"synced\",\n\t\t\t\tnote: input.note ?? null,\n\t\t\t};\n\n\t\t\tlet entryId = 0;\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tconst result = insertRow.run(\n\t\t\t\t\trecord.userId,\n\t\t\t\t\trecord.tsUtc,\n\t\t\t\t\trecord.clientTsUtc,\n\t\t\t\t\trecord.tsLocal,\n\t\t\t\t\trecord.localDate,\n\t\t\t\t\trecord.direction,\n\t\t\t\t\trecord.source,\n\t\t\t\t\trecord.idempotencyKey,\n\t\t\t\t\trecord.syncState,\n\t\t\t\t\trecord.note,\n\t\t\t\t\tinput.actorId ?? null,\n\t\t\t\t\tnow,\n\t\t\t\t\tnow,\n\t\t\t\t\tinput.actorId ?? null,\n\t\t\t\t);\n\t\t\t\tentryId = Number(result.lastInsertRowid);\n\t\t\t\twriteTimeEntryAudit(db, {\n\t\t\t\t\tentryId,\n\t\t\t\t\tuserId: record.userId,\n\t\t\t\t\taction: \"create\",\n\t\t\t\t\tchanges: {\n\t\t\t\t\t\ttsUtc: { old: null, new: record.tsUtc },\n\t\t\t\t\t\tsource: { old: null, new: record.source },\n\t\t\t\t\t},\n\t\t\t\t\tnewTsUtc: record.tsUtc,\n\t\t\t\t\trevision: 1,\n\t\t\t\t\treason: input.reason ?? null,\n\t\t\t\t\tactorId: input.actorId ?? record.userId,\n\t\t\t\t\tactorIp: input.actorIp ?? null,\n\t\t\t\t\tatUtc: now,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst created = read(entryId);\n\t\t\tif (!created) {\n\t\t\t\tthrow new Error(`entry ${entryId} disappeared right after creation`);\n\t\t\t}\n\t\t\treturn { entry: created, created: true };\n\t\t},\n\n\t\tfindById: read,\n\n\t\tlistByRange(userId: number, fromDate: string, toDate: string): EntryRecord[] {\n\t\t\tconst rows = selectByRange.all(userId, fromDate, toDate) as EntryRow[];\n\t\t\treturn rows.map(mapEntryRow);\n\t\t},\n\n\t\tlistByDate(userId: number, localDate: string): EntryRecord[] {\n\t\t\tconst rows = selectByDate.all(userId, localDate) as EntryRow[];\n\t\t\treturn rows.map(mapEntryRow);\n\t\t},\n\n\t\tlistBySyncState(userId: number, syncState: EntrySyncState): EntryRecord[] {\n\t\t\tconst rows = selectBySyncState.all(userId, syncState) as EntryRow[];\n\t\t\treturn rows.map(mapEntryRow);\n\t\t},\n\n\t\tfindByIdempotencyKey(userId: number, idempotencyKey: string): EntryRecord | null {\n\t\t\tconst row = selectByIdempotency.get(userId, idempotencyKey) as EntryRow | undefined;\n\t\t\treturn row ? mapEntryRow(row) : null;\n\t\t},\n\n\t\tsetSyncState(input: {\n\t\t\tid: number;\n\t\t\tsyncState: EntrySyncState;\n\t\t\tactorId: number;\n\t\t\treason?: string | null;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): EntryRecord {\n\t\t\tconst current = read(input.id);\n\t\t\tif (!current) {\n\t\t\t\tthrow new NotFoundError(`entry ${input.id} not found`);\n\t\t\t}\n\t\t\tif (current.syncState === input.syncState) {\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tupdateSyncState.run(input.syncState, now, input.actorId, input.id);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"entry.sync_state\",\n\t\t\t\t\tentity: \"time_entry\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: {\n\t\t\t\t\t\tchanges: { syncState: { old: current.syncState, new: input.syncState } },\n\t\t\t\t\t\treason: input.reason ?? null,\n\t\t\t\t\t},\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst updated = read(input.id);\n\t\t\tif (!updated) {\n\t\t\t\tthrow new Error(`entry ${input.id} disappeared right after the sync state change`);\n\t\t\t}\n\t\t\treturn updated;\n\t\t},\n\n\t\tupdate(input: UpdateEntryInput): EntryRecord {\n\t\t\tconst current = read(input.id);\n\t\t\tif (!current) {\n\t\t\t\tthrow new NotFoundError(`entry ${input.id} not found`);\n\t\t\t}\n\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst nextTsUtc = input.patch.tsUtc ?? current.tsUtc;\n\t\t\tconst nextUserId = input.patch.userId ?? current.userId;\n\t\t\tconst nextNote = input.patch.note === undefined ? current.note : input.patch.note;\n\t\t\tconst nextSource = input.patch.source ?? current.source;\n\n\t\t\tconst changes: Record<string, FieldChange> = {};\n\t\t\tif (nextTsUtc !== current.tsUtc) {\n\t\t\t\tchanges.tsUtc = { old: current.tsUtc, new: nextTsUtc };\n\t\t\t}\n\t\t\tif (nextUserId !== current.userId) {\n\t\t\t\tchanges.userId = { old: current.userId, new: nextUserId };\n\t\t\t}\n\t\t\tif (nextNote !== current.note) {\n\t\t\t\tchanges.note = { old: current.note, new: nextNote };\n\t\t\t}\n\t\t\tif (nextSource !== current.source) {\n\t\t\t\tchanges.source = { old: current.source, new: nextSource };\n\t\t\t}\n\t\t\tif (Object.keys(changes).length === 0) {\n\t\t\t\t// nothing to do, keep the revision untouched\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tconst result = updateRow.run(\n\t\t\t\t\tnextTsUtc,\n\t\t\t\t\tutcToWallTime(nextTsUtc, input.timeZone),\n\t\t\t\t\tresolveLocalDate(nextTsUtc, input.timeZone),\n\t\t\t\t\tnextNote,\n\t\t\t\t\tnextSource,\n\t\t\t\t\tnextUserId,\n\t\t\t\t\tnow,\n\t\t\t\t\tinput.actorId,\n\t\t\t\t\tinput.id,\n\t\t\t\t\tinput.expectedRevision,\n\t\t\t\t);\n\t\t\t\tif (result.changes === 0) {\n\t\t\t\t\tthrow new RevisionConflictError(input.id, read(input.id) ?? current);\n\t\t\t\t}\n\n\t\t\t\twriteTimeEntryAudit(db, {\n\t\t\t\t\tentryId: input.id,\n\t\t\t\t\tuserId: current.userId,\n\t\t\t\t\taction: \"update\",\n\t\t\t\t\tchanges,\n\t\t\t\t\toldTsUtc: current.tsUtc,\n\t\t\t\t\tnewTsUtc: nextTsUtc,\n\t\t\t\t\trevision: input.expectedRevision + 1,\n\t\t\t\t\treason: input.reason ?? null,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\tactorIp: input.actorIp ?? null,\n\t\t\t\t\tatUtc: now,\n\t\t\t\t});\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"entry.update\",\n\t\t\t\t\tentity: \"time_entry\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { changes, reason: input.reason ?? null },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst updated = read(input.id);\n\t\t\tif (!updated) {\n\t\t\t\tthrow new Error(`entry ${input.id} not found after update`);\n\t\t\t}\n\t\t\treturn updated;\n\t\t},\n\n\t\tremove(input: {\n\t\t\tid: number;\n\t\t\tactorId: number;\n\t\t\treason?: string | null;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): boolean {\n\t\t\tconst current = read(input.id);\n\t\t\tif (!current) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeleteRow.run(input.id);\n\t\t\t\twriteTimeEntryAudit(db, {\n\t\t\t\t\tentryId: input.id,\n\t\t\t\t\tuserId: current.userId,\n\t\t\t\t\taction: \"delete\",\n\t\t\t\t\tchanges: { tsUtc: { old: current.tsUtc, new: null } },\n\t\t\t\t\toldTsUtc: current.tsUtc,\n\t\t\t\t\trevision: current.revision + 1,\n\t\t\t\t\treason: input.reason ?? null,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\tactorIp: input.actorIp ?? null,\n\t\t\t\t\tatUtc: now,\n\t\t\t\t});\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"entry.delete\",\n\t\t\t\t\tentity: \"time_entry\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { tsUtc: current.tsUtc, reason: input.reason ?? null },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\t\t\treturn true;\n\t\t},\n\t};\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["resolveLocalDate", "_a", "_b", "_c", "_d", "_e"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var holidays_exports = {};
|
|
20
|
+
__export(holidays_exports, {
|
|
21
|
+
createHolidaysRepository: () => createHolidaysRepository
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(holidays_exports);
|
|
24
|
+
var import_holidays = require("../../domain/holidays");
|
|
25
|
+
var import_time = require("../../util/time");
|
|
26
|
+
var import_errors = require("../../errors");
|
|
27
|
+
var import_audit = require("./audit");
|
|
28
|
+
const DEFAULT_REGION = "DE";
|
|
29
|
+
const COLUMNS = "id, region, year, date, name, key";
|
|
30
|
+
function requireDate(date) {
|
|
31
|
+
const trimmed = date.trim();
|
|
32
|
+
if (!(0, import_time.isDateString)(trimmed)) {
|
|
33
|
+
throw new import_errors.ValidationError(`invalid date "${date}", expected YYYY-MM-DD`);
|
|
34
|
+
}
|
|
35
|
+
return trimmed;
|
|
36
|
+
}
|
|
37
|
+
function createHolidaysRepository(db) {
|
|
38
|
+
const selectByYear = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE year = ? AND region = ? ORDER BY date`);
|
|
39
|
+
const selectByDate = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE date = ? AND region = ?`);
|
|
40
|
+
const selectYears = db.prepare("SELECT DISTINCT year FROM holidays ORDER BY year");
|
|
41
|
+
const selectById = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE id = ?`);
|
|
42
|
+
const insertHoliday = db.prepare(
|
|
43
|
+
`INSERT INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)
|
|
44
|
+
ON CONFLICT(region, date) DO UPDATE SET name = excluded.name, year = excluded.year, key = excluded.key`
|
|
45
|
+
);
|
|
46
|
+
const deleteHoliday = db.prepare("DELETE FROM holidays WHERE id = ?");
|
|
47
|
+
const read = (id) => {
|
|
48
|
+
const row = selectById.get(id);
|
|
49
|
+
return row != null ? row : null;
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
listByYear(year, region = DEFAULT_REGION) {
|
|
53
|
+
return selectByYear.all(year, region);
|
|
54
|
+
},
|
|
55
|
+
years() {
|
|
56
|
+
return selectYears.all().map((row) => row.year);
|
|
57
|
+
},
|
|
58
|
+
isHoliday(date, region = DEFAULT_REGION) {
|
|
59
|
+
return selectByDate.get(date, region) !== void 0;
|
|
60
|
+
},
|
|
61
|
+
dateSet(year, region = DEFAULT_REGION) {
|
|
62
|
+
const rows = selectByYear.all(year, region);
|
|
63
|
+
return new Set(rows.map((row) => row.date));
|
|
64
|
+
},
|
|
65
|
+
ensureYear(input) {
|
|
66
|
+
var _a, _b;
|
|
67
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
68
|
+
let inserted = 0;
|
|
69
|
+
const run = db.transaction(() => {
|
|
70
|
+
const insertMissing = db.prepare(
|
|
71
|
+
"INSERT OR IGNORE INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)"
|
|
72
|
+
);
|
|
73
|
+
for (const holiday of (0, import_holidays.holidaysForYear)(input.year, input.country)) {
|
|
74
|
+
inserted += insertMissing.run(
|
|
75
|
+
input.country,
|
|
76
|
+
input.year,
|
|
77
|
+
holiday.date,
|
|
78
|
+
holiday.name,
|
|
79
|
+
holiday.key
|
|
80
|
+
).changes;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
run();
|
|
84
|
+
if (inserted > 0) {
|
|
85
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
86
|
+
atUtc: now,
|
|
87
|
+
actorId: (_b = input.actorId) != null ? _b : null,
|
|
88
|
+
action: "holiday.ensure",
|
|
89
|
+
entity: "holiday",
|
|
90
|
+
entityId: input.year,
|
|
91
|
+
detail: { year: input.year, country: input.country, inserted }
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return { year: input.year, inserted };
|
|
95
|
+
},
|
|
96
|
+
add(input) {
|
|
97
|
+
var _a, _b;
|
|
98
|
+
const date = requireDate(input.date);
|
|
99
|
+
const name = input.name.trim();
|
|
100
|
+
if (!name) {
|
|
101
|
+
throw new import_errors.ValidationError("name must not be empty");
|
|
102
|
+
}
|
|
103
|
+
const region = (_a = input.region) != null ? _a : DEFAULT_REGION;
|
|
104
|
+
const year = Number(date.slice(0, 4));
|
|
105
|
+
const now = (_b = input.now) != null ? _b : Math.floor(Date.now() / 1e3);
|
|
106
|
+
const existing = selectByDate.get(date, region);
|
|
107
|
+
const run = db.transaction(() => {
|
|
108
|
+
var _a2;
|
|
109
|
+
insertHoliday.run(region, year, date, name, null);
|
|
110
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
111
|
+
atUtc: now,
|
|
112
|
+
actorId: input.actorId,
|
|
113
|
+
action: existing ? "holiday.update" : "holiday.create",
|
|
114
|
+
entity: "holiday",
|
|
115
|
+
entityId: date,
|
|
116
|
+
detail: existing ? { changes: { name: { old: existing.name, new: name } } } : { date, name, region },
|
|
117
|
+
ip: (_a2 = input.actorIp) != null ? _a2 : null
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
run();
|
|
121
|
+
const saved = selectByDate.get(date, region);
|
|
122
|
+
if (!saved) {
|
|
123
|
+
throw new Error(`holiday ${date} disappeared right after the write`);
|
|
124
|
+
}
|
|
125
|
+
return saved;
|
|
126
|
+
},
|
|
127
|
+
remove(input) {
|
|
128
|
+
var _a;
|
|
129
|
+
const existing = read(input.id);
|
|
130
|
+
if (!existing) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
134
|
+
const run = db.transaction(() => {
|
|
135
|
+
var _a2;
|
|
136
|
+
deleteHoliday.run(input.id);
|
|
137
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
138
|
+
atUtc: now,
|
|
139
|
+
actorId: input.actorId,
|
|
140
|
+
action: "holiday.delete",
|
|
141
|
+
entity: "holiday",
|
|
142
|
+
entityId: existing.date,
|
|
143
|
+
detail: { region: existing.region, name: existing.name },
|
|
144
|
+
ip: (_a2 = input.actorIp) != null ? _a2 : null
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
run();
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
153
|
+
0 && (module.exports = {
|
|
154
|
+
createHolidaysRepository
|
|
155
|
+
});
|
|
156
|
+
//# sourceMappingURL=holidays.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/lib/db/repositories/holidays.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Holiday repository (`holidays`).\n *\n * Holidays are stored per region, the region is the country code of the instance (`CH`, `DE`, `AT`).\n * `ensureYear` generates the holidays of a country idempotently and never touches edited rows, so\n * administrators can rename or delete single days.\n */\n\nimport type { Db } from \"../database\";\nimport { holidaysForYear, type HolidayCountry } from \"../../domain/holidays\";\nimport { isDateString } from \"../../util/time\";\nimport { ValidationError } from \"../../errors\";\nimport { writeAuditLog } from \"./audit\";\n\n/** A public holiday as stored in the database. */\nexport interface HolidayRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Region (country code) the holiday belongs to */\n\tregion: string;\n\t/** Year of the holiday (derived from `date`) */\n\tyear: number;\n\t/** Local date, `YYYY-MM-DD` */\n\tdate: string;\n\t/** Display name */\n\tname: string;\n\t/** Key of a seeded holiday (`null` for one that was added by hand) */\n\tkey: string | null;\n}\n\n/** Holiday storage operations. */\nexport interface HolidaysRepository {\n\t/** Holidays of one year, sorted by date */\n\tlistByYear(year: number, region?: string): HolidayRecord[];\n\t/** Years for which holidays are stored */\n\tyears(): number[];\n\t/** True when the date is a holiday of the region */\n\tisHoliday(date: string, region?: string): boolean;\n\t/** All holiday dates of a year as a set (input for the calculation service) */\n\tdateSet(year: number, region?: string): Set<string>;\n\t/** Generates the missing holidays of a country for one year */\n\tensureYear(input: { year: number; country: HolidayCountry; actorId?: number | null; now?: number }): {\n\t\tyear: number;\n\t\tinserted: number;\n\t};\n\t/** Adds a holiday; an existing date of the region is renamed */\n\tadd(input: {\n\t\tdate: string;\n\t\tname: string;\n\t\tregion?: string;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): HolidayRecord;\n\t/** Deletes a holiday by id */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n}\n\ninterface HolidayRow {\n\tid: number;\n\tregion: string;\n\tyear: number;\n\tdate: string;\n\tname: string;\n\tkey: string | null;\n}\n\n/** Default region used when the caller does not pass one. */\nconst DEFAULT_REGION = \"DE\";\n\nconst COLUMNS = \"id, region, year, date, name, key\";\n\n/**\n * Checks and normalises a holiday date.\n *\n * @param date - date to validate\n * @returns the validated date\n */\nfunction requireDate(date: string): string {\n\tconst trimmed = date.trim();\n\tif (!isDateString(trimmed)) {\n\t\tthrow new ValidationError(`invalid date \"${date}\", expected YYYY-MM-DD`);\n\t}\n\treturn trimmed;\n}\n\n/**\n * Creates the holiday repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createHolidaysRepository(db: Db): HolidaysRepository {\n\tconst selectByYear = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE year = ? AND region = ? ORDER BY date`);\n\tconst selectByDate = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE date = ? AND region = ?`);\n\tconst selectYears = db.prepare(\"SELECT DISTINCT year FROM holidays ORDER BY year\");\n\tconst selectById = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE id = ?`);\n\tconst insertHoliday = db.prepare(\n\t\t`INSERT INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\n\t\t ON CONFLICT(region, date) DO UPDATE SET name = excluded.name, year = excluded.year, key = excluded.key`,\n\t);\n\tconst deleteHoliday = db.prepare(\"DELETE FROM holidays WHERE id = ?\");\n\n\tconst read = (id: number): HolidayRecord | null => {\n\t\tconst row = selectById.get(id) as HolidayRow | undefined;\n\t\treturn row ?? null;\n\t};\n\n\treturn {\n\t\tlistByYear(year: number, region: string = DEFAULT_REGION): HolidayRecord[] {\n\t\t\treturn selectByYear.all(year, region) as HolidayRecord[];\n\t\t},\n\n\t\tyears(): number[] {\n\t\t\treturn (selectYears.all() as { year: number }[]).map(row => row.year);\n\t\t},\n\n\t\tisHoliday(date: string, region: string = DEFAULT_REGION): boolean {\n\t\t\treturn selectByDate.get(date, region) !== undefined;\n\t\t},\n\n\t\tdateSet(year: number, region: string = DEFAULT_REGION): Set<string> {\n\t\t\tconst rows = selectByYear.all(year, region) as HolidayRow[];\n\t\t\treturn new Set(rows.map(row => row.date));\n\t\t},\n\n\t\tensureYear(input: { year: number; country: HolidayCountry; actorId?: number | null; now?: number }): {\n\t\t\tyear: number;\n\t\t\tinserted: number;\n\t\t} {\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tlet inserted = 0;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tconst insertMissing = db.prepare(\n\t\t\t\t\t\"INSERT OR IGNORE INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\",\n\t\t\t\t);\n\t\t\t\tfor (const holiday of holidaysForYear(input.year, input.country)) {\n\t\t\t\t\tinserted += insertMissing.run(\n\t\t\t\t\t\tinput.country,\n\t\t\t\t\t\tinput.year,\n\t\t\t\t\t\tholiday.date,\n\t\t\t\t\t\tholiday.name,\n\t\t\t\t\t\tholiday.key,\n\t\t\t\t\t).changes;\n\t\t\t\t}\n\t\t\t});\n\t\t\trun();\n\n\t\t\tif (inserted > 0) {\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId ?? null,\n\t\t\t\t\taction: \"holiday.ensure\",\n\t\t\t\t\tentity: \"holiday\",\n\t\t\t\t\tentityId: input.year,\n\t\t\t\t\tdetail: { year: input.year, country: input.country, inserted },\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn { year: input.year, inserted };\n\t\t},\n\n\t\tadd(input: {\n\t\t\tdate: string;\n\t\t\tname: string;\n\t\t\tregion?: string;\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): HolidayRecord {\n\t\t\tconst date = requireDate(input.date);\n\t\t\tconst name = input.name.trim();\n\t\t\tif (!name) {\n\t\t\t\tthrow new ValidationError(\"name must not be empty\");\n\t\t\t}\n\t\t\tconst region = input.region ?? DEFAULT_REGION;\n\t\t\tconst year = Number(date.slice(0, 4));\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst existing = selectByDate.get(date, region) as HolidayRow | undefined;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\t// a day somebody adds by hand has no key: it keeps the name it was given\n\t\t\t\tinsertHoliday.run(region, year, date, name, null);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: existing ? \"holiday.update\" : \"holiday.create\",\n\t\t\t\t\tentity: \"holiday\",\n\t\t\t\t\tentityId: date,\n\t\t\t\t\tdetail: existing\n\t\t\t\t\t\t? { changes: { name: { old: existing.name, new: name } } }\n\t\t\t\t\t\t: { date, name, region },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst saved = selectByDate.get(date, region) as HolidayRow | undefined;\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`holiday ${date} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn saved;\n\t\t},\n\n\t\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean {\n\t\t\tconst existing = read(input.id);\n\t\t\tif (!existing) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeleteHoliday.run(input.id);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"holiday.delete\",\n\t\t\t\t\tentity: \"holiday\",\n\t\t\t\t\tentityId: existing.date,\n\t\t\t\t\tdetail: { region: existing.region, name: existing.name },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\t\t\treturn true;\n\t\t},\n\t};\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["_a"]
|
|
7
|
+
}
|