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,330 @@
|
|
|
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 states_exports = {};
|
|
20
|
+
__export(states_exports, {
|
|
21
|
+
COMMAND_IDS: () => COMMAND_IDS,
|
|
22
|
+
createCommandStates: () => createCommandStates,
|
|
23
|
+
createCompanyStates: () => createCompanyStates,
|
|
24
|
+
createEventStates: () => createEventStates,
|
|
25
|
+
createInfoStates: () => createInfoStates,
|
|
26
|
+
createUserChannel: () => createUserChannel,
|
|
27
|
+
publishAllUserStates: () => publishAllUserStates,
|
|
28
|
+
publishCompanySnapshot: () => publishCompanySnapshot,
|
|
29
|
+
publishEventSnapshot: () => publishEventSnapshot,
|
|
30
|
+
publishUserSnapshot: () => publishUserSnapshot,
|
|
31
|
+
readCompanySnapshot: () => readCompanySnapshot,
|
|
32
|
+
readUserSnapshot: () => readUserSnapshot
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(states_exports);
|
|
35
|
+
var import_time = require("../util/time");
|
|
36
|
+
const COMMAND_IDS = {
|
|
37
|
+
/** Employee the commands apply to */
|
|
38
|
+
punchUserId: "commands.punchUserId",
|
|
39
|
+
/** Punch in or out for `punchUserId` */
|
|
40
|
+
punch: "commands.punch",
|
|
41
|
+
/** Punch in or out with the configured quick rounding */
|
|
42
|
+
quickPunch: "commands.quickPunch",
|
|
43
|
+
/** Close a month, value `YYYY-MM` */
|
|
44
|
+
closeMonth: "commands.closeMonth",
|
|
45
|
+
/** Recalculate a period, value `YYYY-MM` or `YYYY` */
|
|
46
|
+
recalc: "commands.recalc",
|
|
47
|
+
/** Write a backup of the database */
|
|
48
|
+
backup: "commands.backup"
|
|
49
|
+
};
|
|
50
|
+
function stateObject(name, type, role, options = {}) {
|
|
51
|
+
return {
|
|
52
|
+
type: "state",
|
|
53
|
+
common: {
|
|
54
|
+
name,
|
|
55
|
+
type,
|
|
56
|
+
role,
|
|
57
|
+
read: options.read !== false,
|
|
58
|
+
write: options.write === true,
|
|
59
|
+
...options.unit ? { unit: options.unit } : {}
|
|
60
|
+
},
|
|
61
|
+
native: {}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function channelObject(name) {
|
|
65
|
+
return { type: "channel", common: { name }, native: {} };
|
|
66
|
+
}
|
|
67
|
+
function readUserSnapshot(args) {
|
|
68
|
+
var _a, _b, _c;
|
|
69
|
+
const user = args.users.findById(args.userId);
|
|
70
|
+
if (!user) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const timestamp = (_a = args.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
74
|
+
const today = (0, import_time.localDate)(timestamp, user.timezone);
|
|
75
|
+
const day = args.aggregation.recalculateDay(user.id, today, { now: timestamp });
|
|
76
|
+
const year = Number(today.slice(0, 4));
|
|
77
|
+
const month = Number(today.slice(5, 7));
|
|
78
|
+
const monthAggregate = args.aggregation.recalculateMonth(user.id, year, month, { now: timestamp });
|
|
79
|
+
const yearAggregate = args.aggregation.year(user.id, year);
|
|
80
|
+
return {
|
|
81
|
+
userId: user.id,
|
|
82
|
+
displayName: user.displayName,
|
|
83
|
+
hasOpenEntry: day.hasOpenEntry,
|
|
84
|
+
lastPunchUtc: Math.max((_b = day.firstInUtc) != null ? _b : 0, (_c = day.lastOutUtc) != null ? _c : 0) || null,
|
|
85
|
+
workedMinutes: day.workedMin,
|
|
86
|
+
balanceMinutes: day.balanceMin,
|
|
87
|
+
openConflicts: args.sync.conflicts(user.id).length,
|
|
88
|
+
monthWorkedMinutes: monthAggregate.workedMin,
|
|
89
|
+
monthBalanceMinutes: monthAggregate.balanceMin,
|
|
90
|
+
yearBalanceMinutes: yearAggregate ? yearAggregate.workedMin - yearAggregate.targetMin : 0
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function publishUserSnapshot(port, snapshot) {
|
|
94
|
+
var _a;
|
|
95
|
+
const id = `users.${snapshot.userId}`;
|
|
96
|
+
await port.setState(`${id}.displayName`, snapshot.displayName, true);
|
|
97
|
+
await port.setState(`${id}.present`, snapshot.hasOpenEntry, true);
|
|
98
|
+
await port.setState(`${id}.hasOpenEntry`, snapshot.hasOpenEntry, true);
|
|
99
|
+
await port.setState(`${id}.lastPunch`, (_a = snapshot.lastPunchUtc) != null ? _a : 0, true);
|
|
100
|
+
await port.setState(`${id}.todayWorkedMinutes`, snapshot.workedMinutes, true);
|
|
101
|
+
await port.setState(`${id}.todayBalanceMinutes`, snapshot.balanceMinutes, true);
|
|
102
|
+
await port.setState(`${id}.monthWorkedMinutes`, snapshot.monthWorkedMinutes, true);
|
|
103
|
+
await port.setState(`${id}.monthBalanceMinutes`, snapshot.monthBalanceMinutes, true);
|
|
104
|
+
await port.setState(`${id}.yearBalanceMinutes`, snapshot.yearBalanceMinutes, true);
|
|
105
|
+
await port.setState(`${id}.openConflicts`, snapshot.openConflicts, true);
|
|
106
|
+
}
|
|
107
|
+
async function publishAllUserStates(args) {
|
|
108
|
+
const snapshots = [];
|
|
109
|
+
for (const user of args.users.list({ includeInactive: true })) {
|
|
110
|
+
await createUserChannel(args.port, user.id);
|
|
111
|
+
const snapshot = readUserSnapshot({
|
|
112
|
+
aggregation: args.aggregation,
|
|
113
|
+
users: args.users,
|
|
114
|
+
sync: args.sync,
|
|
115
|
+
userId: user.id,
|
|
116
|
+
now: args.now
|
|
117
|
+
});
|
|
118
|
+
if (!snapshot) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
await publishUserSnapshot(args.port, snapshot);
|
|
122
|
+
snapshots.push(snapshot);
|
|
123
|
+
}
|
|
124
|
+
return snapshots;
|
|
125
|
+
}
|
|
126
|
+
async function createUserChannel(port, userId) {
|
|
127
|
+
const id = `users.${userId}`;
|
|
128
|
+
await port.setObjectNotExists("users", channelObject({ en: "Employees", de: "Mitarbeiter" }));
|
|
129
|
+
await port.setObjectNotExists(id, channelObject({ en: "Employee", de: "Mitarbeiter" }));
|
|
130
|
+
await port.setObjectNotExists(`${id}.displayName`, stateObject({ en: "Name", de: "Name" }, "string", "info.name"));
|
|
131
|
+
await port.setObjectNotExists(
|
|
132
|
+
`${id}.present`,
|
|
133
|
+
stateObject(
|
|
134
|
+
{ en: "Present (working time is running)", de: "Anwesenheit (Arbeitszeit l\xE4uft)" },
|
|
135
|
+
"boolean",
|
|
136
|
+
"switch",
|
|
137
|
+
{ write: true }
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
await port.setObjectNotExists(
|
|
141
|
+
`${id}.hasOpenEntry`,
|
|
142
|
+
stateObject({ en: "Punched in", de: "Eingestempelt" }, "boolean", "indicator.working")
|
|
143
|
+
);
|
|
144
|
+
await port.setObjectNotExists(
|
|
145
|
+
`${id}.lastPunch`,
|
|
146
|
+
stateObject({ en: "Last punch", de: "Letzte Buchung" }, "number", "value.time")
|
|
147
|
+
);
|
|
148
|
+
await port.setObjectNotExists(
|
|
149
|
+
`${id}.todayWorkedMinutes`,
|
|
150
|
+
stateObject({ en: "Worked today", de: "Heute gearbeitet" }, "number", "value", { unit: "min" })
|
|
151
|
+
);
|
|
152
|
+
await port.setObjectNotExists(
|
|
153
|
+
`${id}.todayBalanceMinutes`,
|
|
154
|
+
stateObject({ en: "Balance today", de: "Saldo heute" }, "number", "value", { unit: "min" })
|
|
155
|
+
);
|
|
156
|
+
await port.setObjectNotExists(
|
|
157
|
+
`${id}.monthWorkedMinutes`,
|
|
158
|
+
stateObject({ en: "Worked this month", de: "Diesen Monat gearbeitet" }, "number", "value", { unit: "min" })
|
|
159
|
+
);
|
|
160
|
+
await port.setObjectNotExists(
|
|
161
|
+
`${id}.monthBalanceMinutes`,
|
|
162
|
+
stateObject({ en: "Balance this month", de: "Saldo diesen Monat" }, "number", "value", { unit: "min" })
|
|
163
|
+
);
|
|
164
|
+
await port.setObjectNotExists(
|
|
165
|
+
`${id}.yearBalanceMinutes`,
|
|
166
|
+
stateObject({ en: "Balance this year", de: "Saldo dieses Jahr" }, "number", "value", { unit: "min" })
|
|
167
|
+
);
|
|
168
|
+
await port.setObjectNotExists(
|
|
169
|
+
`${id}.openConflicts`,
|
|
170
|
+
stateObject({ en: "Open conflicts", de: "Offene Konflikte" }, "number", "value")
|
|
171
|
+
);
|
|
172
|
+
return id;
|
|
173
|
+
}
|
|
174
|
+
async function createCommandStates(port) {
|
|
175
|
+
await port.setObjectNotExists("commands", channelObject({ en: "Commands", de: "Befehle" }));
|
|
176
|
+
await port.setObjectNotExists(
|
|
177
|
+
COMMAND_IDS.punchUserId,
|
|
178
|
+
stateObject(
|
|
179
|
+
{ en: "Employee id for punch commands", de: "Mitarbeiter-Id f\xFCr Stempelbefehle" },
|
|
180
|
+
"number",
|
|
181
|
+
"value",
|
|
182
|
+
{
|
|
183
|
+
write: true
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
);
|
|
187
|
+
await port.setObjectNotExists(
|
|
188
|
+
COMMAND_IDS.punch,
|
|
189
|
+
stateObject({ en: "Punch in or out", de: "Ein- oder ausstempeln" }, "boolean", "button", {
|
|
190
|
+
read: false,
|
|
191
|
+
write: true
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
await port.setObjectNotExists(
|
|
195
|
+
COMMAND_IDS.quickPunch,
|
|
196
|
+
stateObject({ en: "Punch with quick rounding", de: "Stempeln mit Schnellrundung" }, "boolean", "button", {
|
|
197
|
+
read: false,
|
|
198
|
+
write: true
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
await port.setObjectNotExists(
|
|
202
|
+
COMMAND_IDS.closeMonth,
|
|
203
|
+
stateObject({ en: "Close month (YYYY-MM)", de: "Monat abschlie\xDFen (JJJJ-MM)" }, "string", "text", {
|
|
204
|
+
write: true
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
await port.setObjectNotExists(
|
|
208
|
+
COMMAND_IDS.recalc,
|
|
209
|
+
stateObject(
|
|
210
|
+
{ en: "Recalculate period (YYYY-MM or YYYY)", de: "Zeitraum neu berechnen (JJJJ-MM oder JJJJ)" },
|
|
211
|
+
"string",
|
|
212
|
+
"text",
|
|
213
|
+
{ write: true }
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
await port.setObjectNotExists(
|
|
217
|
+
COMMAND_IDS.backup,
|
|
218
|
+
stateObject({ en: "Write a database backup", de: "Datenbank-Sicherung schreiben" }, "boolean", "button", {
|
|
219
|
+
read: false,
|
|
220
|
+
write: true
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
async function createInfoStates(port) {
|
|
225
|
+
await port.setObjectNotExists("info", channelObject({ en: "Information", de: "Information" }));
|
|
226
|
+
await port.setObjectNotExists(
|
|
227
|
+
"info.version",
|
|
228
|
+
stateObject({ en: "Adapter version", de: "Adapter-Version" }, "string", "text")
|
|
229
|
+
);
|
|
230
|
+
await port.setObjectNotExists(
|
|
231
|
+
"info.schemaVersion",
|
|
232
|
+
stateObject({ en: "Database schema version", de: "Datenbank-Schemaversion" }, "string", "text")
|
|
233
|
+
);
|
|
234
|
+
await port.setObjectNotExists(
|
|
235
|
+
"info.dbSizeBytes",
|
|
236
|
+
stateObject({ en: "Database size", de: "Datenbank-Gr\xF6\xDFe" }, "number", "value", { unit: "bytes" })
|
|
237
|
+
);
|
|
238
|
+
await port.setObjectNotExists(
|
|
239
|
+
"info.lastError",
|
|
240
|
+
stateObject({ en: "Last error", de: "Letzter Fehler" }, "string", "text")
|
|
241
|
+
);
|
|
242
|
+
await port.setObjectNotExists(
|
|
243
|
+
"info.lastBackup",
|
|
244
|
+
stateObject({ en: "Last backup", de: "Letzte Sicherung" }, "number", "value.time")
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
async function createCompanyStates(port) {
|
|
248
|
+
await port.setObjectNotExists("company", channelObject({ en: "Company", de: "Firma" }));
|
|
249
|
+
await port.setObjectNotExists(
|
|
250
|
+
"company.presentCount",
|
|
251
|
+
stateObject({ en: "Present employees", de: "Anwesende Mitarbeiter" }, "number", "value")
|
|
252
|
+
);
|
|
253
|
+
await port.setObjectNotExists(
|
|
254
|
+
"company.present",
|
|
255
|
+
stateObject({ en: "Who is present", de: "Wer ist anwesend" }, "string", "text")
|
|
256
|
+
);
|
|
257
|
+
await port.setObjectNotExists(
|
|
258
|
+
"company.openConflicts",
|
|
259
|
+
stateObject({ en: "Open conflicts", de: "Offene Konflikte" }, "number", "value")
|
|
260
|
+
);
|
|
261
|
+
await port.setObjectNotExists(
|
|
262
|
+
"company.lastPunch",
|
|
263
|
+
stateObject({ en: "Last punch", de: "Letzte Buchung" }, "number", "value.time")
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
function readCompanySnapshot(snapshots) {
|
|
267
|
+
const present = snapshots.filter((snapshot) => snapshot.hasOpenEntry);
|
|
268
|
+
return {
|
|
269
|
+
presentCount: present.length,
|
|
270
|
+
present: present.map((snapshot) => snapshot.displayName).join(", "),
|
|
271
|
+
openConflicts: snapshots.reduce((sum, snapshot) => sum + snapshot.openConflicts, 0),
|
|
272
|
+
lastPunchUtc: snapshots.reduce(
|
|
273
|
+
(newest, snapshot) => snapshot.lastPunchUtc !== null && (newest === null || snapshot.lastPunchUtc > newest) ? snapshot.lastPunchUtc : newest,
|
|
274
|
+
null
|
|
275
|
+
)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
async function publishCompanySnapshot(port, snapshot) {
|
|
279
|
+
var _a;
|
|
280
|
+
await port.setState("company.presentCount", snapshot.presentCount, true);
|
|
281
|
+
await port.setState("company.present", snapshot.present, true);
|
|
282
|
+
await port.setState("company.openConflicts", snapshot.openConflicts, true);
|
|
283
|
+
await port.setState("company.lastPunch", (_a = snapshot.lastPunchUtc) != null ? _a : 0, true);
|
|
284
|
+
}
|
|
285
|
+
async function createEventStates(port) {
|
|
286
|
+
await port.setObjectNotExists("events", channelObject({ en: "Events", de: "Ereignisse" }));
|
|
287
|
+
await port.setObjectNotExists(
|
|
288
|
+
"events.lastAt",
|
|
289
|
+
stateObject({ en: "Last event at", de: "Letztes Ereignis um" }, "number", "value.time")
|
|
290
|
+
);
|
|
291
|
+
await port.setObjectNotExists(
|
|
292
|
+
"events.lastType",
|
|
293
|
+
stateObject({ en: "Kind of the last event", de: "Art des letzten Ereignisses" }, "string", "text")
|
|
294
|
+
);
|
|
295
|
+
await port.setObjectNotExists(
|
|
296
|
+
"events.lastUser",
|
|
297
|
+
stateObject({ en: "Employee of the last event", de: "Mitarbeiter des letzten Ereignisses" }, "string", "text")
|
|
298
|
+
);
|
|
299
|
+
await port.setObjectNotExists(
|
|
300
|
+
"events.lastDirection",
|
|
301
|
+
stateObject({ en: "Direction of the last punch", de: "Richtung der letzten Buchung" }, "string", "text")
|
|
302
|
+
);
|
|
303
|
+
await port.setObjectNotExists(
|
|
304
|
+
"events.lastSource",
|
|
305
|
+
stateObject({ en: "Source of the last event", de: "Quelle des letzten Ereignisses" }, "string", "text")
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
async function publishEventSnapshot(port, snapshot) {
|
|
309
|
+
await port.setState("events.lastAt", snapshot.atUtc, true);
|
|
310
|
+
await port.setState("events.lastType", snapshot.type, true);
|
|
311
|
+
await port.setState("events.lastUser", snapshot.userName, true);
|
|
312
|
+
await port.setState("events.lastDirection", snapshot.direction, true);
|
|
313
|
+
await port.setState("events.lastSource", snapshot.source, true);
|
|
314
|
+
}
|
|
315
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
316
|
+
0 && (module.exports = {
|
|
317
|
+
COMMAND_IDS,
|
|
318
|
+
createCommandStates,
|
|
319
|
+
createCompanyStates,
|
|
320
|
+
createEventStates,
|
|
321
|
+
createInfoStates,
|
|
322
|
+
createUserChannel,
|
|
323
|
+
publishAllUserStates,
|
|
324
|
+
publishCompanySnapshot,
|
|
325
|
+
publishEventSnapshot,
|
|
326
|
+
publishUserSnapshot,
|
|
327
|
+
readCompanySnapshot,
|
|
328
|
+
readUserSnapshot
|
|
329
|
+
});
|
|
330
|
+
//# sourceMappingURL=states.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/adapter/states.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * States published by the adapter.\n *\n * The adapter exposes a small, stable state tree so that other adapters (and the user's dashboards) can show\n * and trigger time tracking without speaking to the REST API:\n *\n * ```\n * users.<userId>.displayName / present / hasOpenEntry / lastPunch / todayWorkedMinutes / todayBalanceMinutes\n * users.<userId>.openConflicts\n * commands.punchUserId / punch / quickPunch / closeMonth / recalc\n * ```\n *\n * All objects are created with `setObjectNotExists`, so existing instances keep their settings. The logic works\n * on a tiny port interface (`StatePort`) \u2014 the adapter instance in production, a recorder in the tests.\n */\n\nimport type { UsersRepository } from \"../db/repositories/users\";\nimport type { AggregationService } from \"../services/aggregation\";\nimport type { SyncService } from \"../services/sync\";\nimport { localDate as resolveLocalDate } from \"../util/time\";\n\n/** Subset of the adapter API the state layer needs. */\nexport interface StatePort {\n\t/**\n\t * Creates an object when it does not exist yet.\n\t *\n\t * @param id - full state id (including the instance prefix)\n\t * @param object - object definition\n\t */\n\tsetObjectNotExists(id: string, object: ioBroker.SettableObject): Promise<unknown> | void;\n\t/**\n\t * Writes a state value.\n\t *\n\t * @param id - full state id (including the instance prefix)\n\t * @param value - value to write\n\t * @param ack - true for confirmed values\n\t */\n\tsetState(id: string, value: ioBroker.StateValue, ack?: boolean): Promise<unknown> | void;\n}\n\n/** Figures of one employee shown in the state tree. */\nexport interface UserSnapshot {\n\t/** Database id of the employee */\n\tuserId: number;\n\t/** Shown name */\n\tdisplayName: string;\n\t/** True when the last punch of today has no counterpart */\n\thasOpenEntry: boolean;\n\t/** Instant of the last punch of the day, `null` when there is none */\n\tlastPunchUtc: number | null;\n\t/** Net working time of today in minutes */\n\tworkedMinutes: number;\n\t/** Balance of today in minutes */\n\tbalanceMinutes: number;\n\t/** Punches waiting for a decision */\n\topenConflicts: number;\n\t/** Net working time of the current month in minutes */\n\tmonthWorkedMinutes: number;\n\t/** Balance of the current month in minutes */\n\tmonthBalanceMinutes: number;\n\t/** Balance of the current year in minutes */\n\tyearBalanceMinutes: number;\n}\n\n/** Figures of the whole company, published next to the employees. */\nexport interface CompanySnapshot {\n\t/** Employees that are clocked in right now */\n\tpresentCount: number;\n\t/** Names of those employees, separated by a comma (empty when nobody is present) */\n\tpresent: string;\n\t/** Punches of all employees waiting for a decision */\n\topenConflicts: number;\n\t/** Instant of the newest punch of today, `null` when nobody punched */\n\tlastPunchUtc: number | null;\n}\n\n/** Newest event of the instance, mirrored into the state tree. */\nexport interface EventSnapshot {\n\t/** Kind of the event, e.g. `punch` */\n\ttype: string;\n\t/** Instant the event belongs to */\n\tatUtc: number;\n\t/** Name of the employee the event belongs to, empty for instance wide events */\n\tuserName: string;\n\t/** Direction of a punch (`in`/`out`), empty for other events */\n\tdirection: string;\n\t/** Where the change came from, e.g. `web` or `terminal`, empty when unknown */\n\tsource: string;\n}\n\n/** Names of the writable command states. */\nexport const COMMAND_IDS = {\n\t/** Employee the commands apply to */\n\tpunchUserId: \"commands.punchUserId\",\n\t/** Punch in or out for `punchUserId` */\n\tpunch: \"commands.punch\",\n\t/** Punch in or out with the configured quick rounding */\n\tquickPunch: \"commands.quickPunch\",\n\t/** Close a month, value `YYYY-MM` */\n\tcloseMonth: \"commands.closeMonth\",\n\t/** Recalculate a period, value `YYYY-MM` or `YYYY` */\n\trecalc: \"commands.recalc\",\n\t/** Write a backup of the database */\n\tbackup: \"commands.backup\",\n} as const;\n\n/**\n * Builds a state object definition.\n *\n * @param name - name in English and German\n * @param name.en - English name\n * @param name.de - German name\n * @param type - state type\n * @param role - state role\n * @param options - additional flags\n * @param options.read - value can be read (default true)\n * @param options.write - value can be written (default false)\n * @param options.unit - unit of the value\n * @returns object definition\n */\nfunction stateObject(\n\tname: { en: string; de: string },\n\ttype: ioBroker.CommonType,\n\trole: string,\n\toptions: { read?: boolean; write?: boolean; unit?: string } = {},\n): ioBroker.SettableObject {\n\treturn {\n\t\ttype: \"state\",\n\t\tcommon: {\n\t\t\tname,\n\t\t\ttype,\n\t\t\trole,\n\t\t\tread: options.read !== false,\n\t\t\twrite: options.write === true,\n\t\t\t...(options.unit ? { unit: options.unit } : {}),\n\t\t},\n\t\tnative: {},\n\t};\n}\n\n/**\n * Builds a channel object definition.\n *\n * @param name - name in English and German\n * @param name.en - English name\n * @param name.de - German name\n * @returns object definition\n */\nfunction channelObject(name: { en: string; de: string }): ioBroker.SettableObject {\n\treturn { type: \"channel\", common: { name }, native: {} };\n}\n\n/**\n * Reads the figures of an employee for the state tree.\n *\n * @param args - data sources and employee\n * @param args.aggregation - aggregation service\n * @param args.users - user storage\n * @param args.sync - synchronisation service (open conflicts)\n * @param args.userId - database id of the employee\n * @param args.now - instant of the snapshot\n * @returns snapshot of the figures\n */\nexport function readUserSnapshot(args: {\n\taggregation: AggregationService;\n\tusers: UsersRepository;\n\tsync: SyncService;\n\tuserId: number;\n\tnow?: number;\n}): UserSnapshot | null {\n\tconst user = args.users.findById(args.userId);\n\tif (!user) {\n\t\treturn null;\n\t}\n\n\tconst timestamp = args.now ?? Math.floor(Date.now() / 1000);\n\tconst today = resolveLocalDate(timestamp, user.timezone);\n\tconst day = args.aggregation.recalculateDay(user.id, today, { now: timestamp });\n\t// the month is recalculated like the month view does it; the year is read, so the publishing stays cheap\n\tconst year = Number(today.slice(0, 4));\n\tconst month = Number(today.slice(5, 7));\n\tconst monthAggregate = args.aggregation.recalculateMonth(user.id, year, month, { now: timestamp });\n\tconst yearAggregate = args.aggregation.year(user.id, year);\n\n\treturn {\n\t\tuserId: user.id,\n\t\tdisplayName: user.displayName,\n\t\thasOpenEntry: day.hasOpenEntry,\n\t\tlastPunchUtc: Math.max(day.firstInUtc ?? 0, day.lastOutUtc ?? 0) || null,\n\t\tworkedMinutes: day.workedMin,\n\t\tbalanceMinutes: day.balanceMin,\n\t\topenConflicts: args.sync.conflicts(user.id).length,\n\t\tmonthWorkedMinutes: monthAggregate.workedMin,\n\t\tmonthBalanceMinutes: monthAggregate.balanceMin,\n\t\tyearBalanceMinutes: yearAggregate ? yearAggregate.workedMin - yearAggregate.targetMin : 0,\n\t};\n}\n\n/**\n * Publishes the figures of one employee.\n *\n * @param port - state port\n * @param snapshot - figures to publish\n */\nexport async function publishUserSnapshot(port: StatePort, snapshot: UserSnapshot): Promise<void> {\n\tconst id = `users.${snapshot.userId}`;\n\tawait port.setState(`${id}.displayName`, snapshot.displayName, true);\n\tawait port.setState(`${id}.present`, snapshot.hasOpenEntry, true);\n\tawait port.setState(`${id}.hasOpenEntry`, snapshot.hasOpenEntry, true);\n\tawait port.setState(`${id}.lastPunch`, snapshot.lastPunchUtc ?? 0, true);\n\tawait port.setState(`${id}.todayWorkedMinutes`, snapshot.workedMinutes, true);\n\tawait port.setState(`${id}.todayBalanceMinutes`, snapshot.balanceMinutes, true);\n\tawait port.setState(`${id}.monthWorkedMinutes`, snapshot.monthWorkedMinutes, true);\n\tawait port.setState(`${id}.monthBalanceMinutes`, snapshot.monthBalanceMinutes, true);\n\tawait port.setState(`${id}.yearBalanceMinutes`, snapshot.yearBalanceMinutes, true);\n\tawait port.setState(`${id}.openConflicts`, snapshot.openConflicts, true);\n}\n\n/**\n * Creates the channels of all employees and publishes their current figures.\n *\n * @param args - state port and data sources\n * @param args.port - state port\n * @param args.aggregation - aggregation service\n * @param args.users - user storage\n * @param args.sync - synchronisation service\n * @param args.now - instant of the snapshot\n * @returns the published snapshots\n */\nexport async function publishAllUserStates(args: {\n\tport: StatePort;\n\taggregation: AggregationService;\n\tusers: UsersRepository;\n\tsync: SyncService;\n\tnow?: number;\n}): Promise<UserSnapshot[]> {\n\tconst snapshots: UserSnapshot[] = [];\n\tfor (const user of args.users.list({ includeInactive: true })) {\n\t\tawait createUserChannel(args.port, user.id);\n\t\tconst snapshot = readUserSnapshot({\n\t\t\taggregation: args.aggregation,\n\t\t\tusers: args.users,\n\t\t\tsync: args.sync,\n\t\t\tuserId: user.id,\n\t\t\tnow: args.now,\n\t\t});\n\t\tif (!snapshot) {\n\t\t\tcontinue;\n\t\t}\n\t\tawait publishUserSnapshot(args.port, snapshot);\n\t\tsnapshots.push(snapshot);\n\t}\n\treturn snapshots;\n}\n\n/**\n * Creates the channel of one employee, including the shared parent channel `users`.\n *\n * The parent channel matters: the ioBroker object structure check (`E3009`) requires every state to have its\n * parents, so `users.<userId>.*` below a missing `users` produced 126 findings in the repository check\n * (PR #6697). `setObjectNotExists` keeps the call idempotent.\n *\n * @param port - state port\n * @param userId - database id of the employee\n * @returns the channel id\n */\nexport async function createUserChannel(port: StatePort, userId: number): Promise<string> {\n\tconst id = `users.${userId}`;\n\tawait port.setObjectNotExists(\"users\", channelObject({ en: \"Employees\", de: \"Mitarbeiter\" }));\n\tawait port.setObjectNotExists(id, channelObject({ en: \"Employee\", de: \"Mitarbeiter\" }));\n\tawait port.setObjectNotExists(`${id}.displayName`, stateObject({ en: \"Name\", de: \"Name\" }, \"string\", \"info.name\"));\n\t// the writable twin of `hasOpenEntry`: a script, a fingerprint reader or a dashboard writes it to say that\n\t// somebody arrived (`true`) or left (`false`) \u2014 see `presence.ts` for what the adapter does with it\n\tawait port.setObjectNotExists(\n\t\t`${id}.present`,\n\t\tstateObject(\n\t\t\t{ en: \"Present (working time is running)\", de: \"Anwesenheit (Arbeitszeit l\u00E4uft)\" },\n\t\t\t\"boolean\",\n\t\t\t\"switch\",\n\t\t\t{ write: true },\n\t\t),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.hasOpenEntry`,\n\t\tstateObject({ en: \"Punched in\", de: \"Eingestempelt\" }, \"boolean\", \"indicator.working\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.lastPunch`,\n\t\tstateObject({ en: \"Last punch\", de: \"Letzte Buchung\" }, \"number\", \"value.time\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.todayWorkedMinutes`,\n\t\tstateObject({ en: \"Worked today\", de: \"Heute gearbeitet\" }, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.todayBalanceMinutes`,\n\t\tstateObject({ en: \"Balance today\", de: \"Saldo heute\" }, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.monthWorkedMinutes`,\n\t\tstateObject({ en: \"Worked this month\", de: \"Diesen Monat gearbeitet\" }, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.monthBalanceMinutes`,\n\t\tstateObject({ en: \"Balance this month\", de: \"Saldo diesen Monat\" }, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.yearBalanceMinutes`,\n\t\tstateObject({ en: \"Balance this year\", de: \"Saldo dieses Jahr\" }, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.openConflicts`,\n\t\tstateObject({ en: \"Open conflicts\", de: \"Offene Konflikte\" }, \"number\", \"value\"),\n\t);\n\treturn id;\n}\n\n/**\n * Creates the writable command states.\n *\n * @param port - state port\n */\nexport async function createCommandStates(port: StatePort): Promise<void> {\n\tawait port.setObjectNotExists(\"commands\", channelObject({ en: \"Commands\", de: \"Befehle\" }));\n\tawait port.setObjectNotExists(\n\t\tCOMMAND_IDS.punchUserId,\n\t\tstateObject(\n\t\t\t{ en: \"Employee id for punch commands\", de: \"Mitarbeiter-Id f\u00FCr Stempelbefehle\" },\n\t\t\t\"number\",\n\t\t\t\"value\",\n\t\t\t{\n\t\t\t\twrite: true,\n\t\t\t},\n\t\t),\n\t);\n\tawait port.setObjectNotExists(\n\t\tCOMMAND_IDS.punch,\n\t\tstateObject({ en: \"Punch in or out\", de: \"Ein- oder ausstempeln\" }, \"boolean\", \"button\", {\n\t\t\tread: false,\n\t\t\twrite: true,\n\t\t}),\n\t);\n\tawait port.setObjectNotExists(\n\t\tCOMMAND_IDS.quickPunch,\n\t\tstateObject({ en: \"Punch with quick rounding\", de: \"Stempeln mit Schnellrundung\" }, \"boolean\", \"button\", {\n\t\t\tread: false,\n\t\t\twrite: true,\n\t\t}),\n\t);\n\tawait port.setObjectNotExists(\n\t\tCOMMAND_IDS.closeMonth,\n\t\tstateObject({ en: \"Close month (YYYY-MM)\", de: \"Monat abschlie\u00DFen (JJJJ-MM)\" }, \"string\", \"text\", {\n\t\t\twrite: true,\n\t\t}),\n\t);\n\tawait port.setObjectNotExists(\n\t\tCOMMAND_IDS.recalc,\n\t\tstateObject(\n\t\t\t{ en: \"Recalculate period (YYYY-MM or YYYY)\", de: \"Zeitraum neu berechnen (JJJJ-MM oder JJJJ)\" },\n\t\t\t\"string\",\n\t\t\t\"text\",\n\t\t\t{ write: true },\n\t\t),\n\t);\n\tawait port.setObjectNotExists(\n\t\tCOMMAND_IDS.backup,\n\t\tstateObject({ en: \"Write a database backup\", de: \"Datenbank-Sicherung schreiben\" }, \"boolean\", \"button\", {\n\t\t\tread: false,\n\t\t\twrite: true,\n\t\t}),\n\t);\n}\n\n/**\n * Creates the informational states of the instance.\n *\n * @param port - state port\n */\nexport async function createInfoStates(port: StatePort): Promise<void> {\n\tawait port.setObjectNotExists(\"info\", channelObject({ en: \"Information\", de: \"Information\" }));\n\tawait port.setObjectNotExists(\n\t\t\"info.version\",\n\t\tstateObject({ en: \"Adapter version\", de: \"Adapter-Version\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"info.schemaVersion\",\n\t\tstateObject({ en: \"Database schema version\", de: \"Datenbank-Schemaversion\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"info.dbSizeBytes\",\n\t\tstateObject({ en: \"Database size\", de: \"Datenbank-Gr\u00F6\u00DFe\" }, \"number\", \"value\", { unit: \"bytes\" }),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"info.lastError\",\n\t\tstateObject({ en: \"Last error\", de: \"Letzter Fehler\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"info.lastBackup\",\n\t\tstateObject({ en: \"Last backup\", de: \"Letzte Sicherung\" }, \"number\", \"value.time\"),\n\t);\n}\n\n/**\n * Creates the states of the company.\n *\n * @param port - state port\n */\nexport async function createCompanyStates(port: StatePort): Promise<void> {\n\tawait port.setObjectNotExists(\"company\", channelObject({ en: \"Company\", de: \"Firma\" }));\n\tawait port.setObjectNotExists(\n\t\t\"company.presentCount\",\n\t\tstateObject({ en: \"Present employees\", de: \"Anwesende Mitarbeiter\" }, \"number\", \"value\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"company.present\",\n\t\tstateObject({ en: \"Who is present\", de: \"Wer ist anwesend\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"company.openConflicts\",\n\t\tstateObject({ en: \"Open conflicts\", de: \"Offene Konflikte\" }, \"number\", \"value\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"company.lastPunch\",\n\t\tstateObject({ en: \"Last punch\", de: \"Letzte Buchung\" }, \"number\", \"value.time\"),\n\t);\n}\n\n/**\n * Reads the company figures out of the figures of the employees.\n *\n * @param snapshots - figures of all employees\n * @returns the company figures\n */\nexport function readCompanySnapshot(snapshots: UserSnapshot[]): CompanySnapshot {\n\tconst present = snapshots.filter(snapshot => snapshot.hasOpenEntry);\n\treturn {\n\t\tpresentCount: present.length,\n\t\tpresent: present.map(snapshot => snapshot.displayName).join(\", \"),\n\t\topenConflicts: snapshots.reduce((sum, snapshot) => sum + snapshot.openConflicts, 0),\n\t\tlastPunchUtc: snapshots.reduce<number | null>(\n\t\t\t(newest, snapshot) =>\n\t\t\t\tsnapshot.lastPunchUtc !== null && (newest === null || snapshot.lastPunchUtc > newest)\n\t\t\t\t\t? snapshot.lastPunchUtc\n\t\t\t\t\t: newest,\n\t\t\tnull,\n\t\t),\n\t};\n}\n\n/**\n * Publishes the company figures.\n *\n * @param port - state port\n * @param snapshot - figures to publish\n */\nexport async function publishCompanySnapshot(port: StatePort, snapshot: CompanySnapshot): Promise<void> {\n\tawait port.setState(\"company.presentCount\", snapshot.presentCount, true);\n\tawait port.setState(\"company.present\", snapshot.present, true);\n\tawait port.setState(\"company.openConflicts\", snapshot.openConflicts, true);\n\tawait port.setState(\"company.lastPunch\", snapshot.lastPunchUtc ?? 0, true);\n}\n\n/**\n * Creates the event states.\n *\n * @param port - state port\n */\nexport async function createEventStates(port: StatePort): Promise<void> {\n\tawait port.setObjectNotExists(\"events\", channelObject({ en: \"Events\", de: \"Ereignisse\" }));\n\tawait port.setObjectNotExists(\n\t\t\"events.lastAt\",\n\t\tstateObject({ en: \"Last event at\", de: \"Letztes Ereignis um\" }, \"number\", \"value.time\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"events.lastType\",\n\t\tstateObject({ en: \"Kind of the last event\", de: \"Art des letzten Ereignisses\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"events.lastUser\",\n\t\tstateObject({ en: \"Employee of the last event\", de: \"Mitarbeiter des letzten Ereignisses\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"events.lastDirection\",\n\t\tstateObject({ en: \"Direction of the last punch\", de: \"Richtung der letzten Buchung\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"events.lastSource\",\n\t\tstateObject({ en: \"Source of the last event\", de: \"Quelle des letzten Ereignisses\" }, \"string\", \"text\"),\n\t);\n}\n\n/**\n * Publishes the newest event.\n *\n * @param port - state port\n * @param snapshot - event to publish\n */\nexport async function publishEventSnapshot(port: StatePort, snapshot: EventSnapshot): Promise<void> {\n\tawait port.setState(\"events.lastAt\", snapshot.atUtc, true);\n\tawait port.setState(\"events.lastType\", snapshot.type, true);\n\tawait port.setState(\"events.lastUser\", snapshot.userName, true);\n\tawait port.setState(\"events.lastDirection\", snapshot.direction, true);\n\tawait port.setState(\"events.lastSource\", snapshot.source, true);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["resolveLocalDate"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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 triggers_exports = {};
|
|
20
|
+
__export(triggers_exports, {
|
|
21
|
+
evaluateTrigger: () => evaluateTrigger,
|
|
22
|
+
isToggleCondition: () => isToggleCondition,
|
|
23
|
+
resolveTriggerUser: () => resolveTriggerUser,
|
|
24
|
+
triggerText: () => triggerText
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(triggers_exports);
|
|
27
|
+
function triggerText(value) {
|
|
28
|
+
return value === null ? "" : String(value).trim();
|
|
29
|
+
}
|
|
30
|
+
function isToggleCondition(condition) {
|
|
31
|
+
const normalized = (condition != null ? condition : "").trim().toLowerCase();
|
|
32
|
+
return normalized === "toggle" || normalized === "*";
|
|
33
|
+
}
|
|
34
|
+
function resolveTriggerUser(value, users) {
|
|
35
|
+
var _a;
|
|
36
|
+
const text = triggerText(value);
|
|
37
|
+
if (!text) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (/^\d+$/.test(text)) {
|
|
41
|
+
const byId = users.findById(Number(text));
|
|
42
|
+
if (byId) {
|
|
43
|
+
return byId;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const byLogin = users.findByLogin(text);
|
|
47
|
+
if (byLogin) {
|
|
48
|
+
return byLogin;
|
|
49
|
+
}
|
|
50
|
+
const lower = text.toLowerCase();
|
|
51
|
+
return (_a = users.list().find((user) => user.displayName.toLowerCase() === lower)) != null ? _a : null;
|
|
52
|
+
}
|
|
53
|
+
function evaluateTrigger(rule, context, users) {
|
|
54
|
+
var _a, _b, _c;
|
|
55
|
+
const text = triggerText(context.value);
|
|
56
|
+
if (!text) {
|
|
57
|
+
return { fire: false, userId: null, reason: "empty value" };
|
|
58
|
+
}
|
|
59
|
+
if (context.previous !== null && context.previous === text) {
|
|
60
|
+
return { fire: false, userId: null, reason: `value ${text} did not change` };
|
|
61
|
+
}
|
|
62
|
+
if (rule.cooldownSec > 0 && rule.lastFiredAt !== null && rule.lastFiredAt + rule.cooldownSec > context.now) {
|
|
63
|
+
return { fire: false, userId: null, reason: `cooldown of ${rule.cooldownSec} s is still running` };
|
|
64
|
+
}
|
|
65
|
+
let user = null;
|
|
66
|
+
if (rule.mode === "user") {
|
|
67
|
+
user = resolveTriggerUser(context.value, users);
|
|
68
|
+
if (!user) {
|
|
69
|
+
return { fire: false, userId: null, reason: `no employee matches "${text}"` };
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
if (!isToggleCondition(rule.condition) && text !== ((_a = rule.condition) != null ? _a : "")) {
|
|
73
|
+
return { fire: false, userId: null, reason: `value ${text} does not match ${(_b = rule.condition) != null ? _b : "?"}` };
|
|
74
|
+
}
|
|
75
|
+
user = rule.userId === null ? null : users.findById(rule.userId);
|
|
76
|
+
if (!user) {
|
|
77
|
+
return { fire: false, userId: null, reason: `employee ${(_c = rule.userId) != null ? _c : "?"} does not exist` };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!user.isActive) {
|
|
81
|
+
return { fire: false, userId: user.id, reason: `${user.displayName} is deactivated` };
|
|
82
|
+
}
|
|
83
|
+
return { fire: true, userId: user.id, reason: `${rule.action} for ${user.displayName}` };
|
|
84
|
+
}
|
|
85
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
86
|
+
0 && (module.exports = {
|
|
87
|
+
evaluateTrigger,
|
|
88
|
+
isToggleCondition,
|
|
89
|
+
resolveTriggerUser,
|
|
90
|
+
triggerText
|
|
91
|
+
});
|
|
92
|
+
//# sourceMappingURL=triggers.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/adapter/triggers.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Trigger rules of the adapter.\n *\n * A rule watches a state of another adapter and turns a write on it into a punch or a presence change \u2014 the\n * ioBroker way of connecting a fingerprint reader, a button or a door contact. This module only *decides*:\n * the adapter subscribes to the states and runs the decision, so the logic itself stays free of ioBroker.\n *\n * A rule fires on a **change** of the value (a reader that repeats itself is harmless) and only after the\n * configured cooldown. In mode `condition` the value has to equal the stored one and the punch goes to the\n * stored employee; in mode `user` the value names the employee \u2014 as id, login or display name.\n */\n\nimport type { TriggerRuleRecord } from \"../db/repositories/triggers\";\nimport type { UserRecord, UsersRepository } from \"../db/repositories/users\";\n\n/** Users the trigger logic resolves employees with. */\nexport type TriggerUsers = Pick<UsersRepository, \"findById\" | \"findByLogin\" | \"list\">;\n\n/** What the adapter knows about a write on a watched state. */\nexport interface TriggerContext {\n\t/** Value that was written */\n\tvalue: ioBroker.StateValue;\n\t/** Value seen before, `null` on the first look after the start */\n\tprevious: string | null;\n\t/** Instant of the write, UTC epoch seconds */\n\tnow: number;\n}\n\n/** Outcome of a rule check. */\nexport interface TriggerDecision {\n\t/** True when the action has to run */\n\tfire: boolean;\n\t/** Employee the action applies to, `null` when the rule did not fire */\n\tuserId: number | null;\n\t/** Why the rule fired or not \u2014 goes to the log and to the tests */\n\treason: string;\n}\n\n/**\n * Normalises a state value for the comparison with a rule.\n *\n * @param value - value written on a state\n * @returns the text form without surrounding blanks\n */\nexport function triggerText(value: ioBroker.StateValue): string {\n\treturn value === null ? \"\" : String(value).trim();\n}\n\n/**\n * True when the stored value means \"any change\".\n *\n * A switch, a button or a reader that only reports *that* something happened carries `true` and `false`\n * alternately \u2014 a fixed value would only fire for one of the two directions. `toggle` (or `*`) makes the rule\n * fire for every change, and the action `punch` then picks the direction itself.\n *\n * @param condition - value stored in the rule\n * @returns true when the rule fires for every changed value\n */\nexport function isToggleCondition(condition: string | null | undefined): boolean {\n\tconst normalized = (condition ?? \"\").trim().toLowerCase();\n\treturn normalized === \"toggle\" || normalized === \"*\";\n}\n\n/**\n * Resolves the employee a value of mode `user` names.\n *\n * @param value - value written on the watched state\n * @param users - user storage\n * @returns the employee or `null` when nothing matches\n */\nexport function resolveTriggerUser(value: ioBroker.StateValue, users: TriggerUsers): UserRecord | null {\n\tconst text = triggerText(value);\n\tif (!text) {\n\t\treturn null;\n\t}\n\n\tif (/^\\d+$/.test(text)) {\n\t\tconst byId = users.findById(Number(text));\n\t\tif (byId) {\n\t\t\treturn byId;\n\t\t}\n\t}\n\n\tconst byLogin = users.findByLogin(text);\n\tif (byLogin) {\n\t\treturn byLogin;\n\t}\n\n\tconst lower = text.toLowerCase();\n\treturn users.list().find(user => user.displayName.toLowerCase() === lower) ?? null;\n}\n\n/**\n * Decides whether a rule fires for a write and for which employee.\n *\n * @param rule - rule to check\n * @param context - value, previous value and instant of the write\n * @param users - user storage\n * @returns the decision, including the reason for the log\n */\nexport function evaluateTrigger(\n\trule: TriggerRuleRecord,\n\tcontext: TriggerContext,\n\tusers: TriggerUsers,\n): TriggerDecision {\n\tconst text = triggerText(context.value);\n\tif (!text) {\n\t\treturn { fire: false, userId: null, reason: \"empty value\" };\n\t}\n\tif (context.previous !== null && context.previous === text) {\n\t\treturn { fire: false, userId: null, reason: `value ${text} did not change` };\n\t}\n\tif (rule.cooldownSec > 0 && rule.lastFiredAt !== null && rule.lastFiredAt + rule.cooldownSec > context.now) {\n\t\treturn { fire: false, userId: null, reason: `cooldown of ${rule.cooldownSec} s is still running` };\n\t}\n\n\tlet user: UserRecord | null = null;\n\tif (rule.mode === \"user\") {\n\t\tuser = resolveTriggerUser(context.value, users);\n\t\tif (!user) {\n\t\t\treturn { fire: false, userId: null, reason: `no employee matches \"${text}\"` };\n\t\t}\n\t} else {\n\t\tif (!isToggleCondition(rule.condition) && text !== (rule.condition ?? \"\")) {\n\t\t\treturn { fire: false, userId: null, reason: `value ${text} does not match ${rule.condition ?? \"?\"}` };\n\t\t}\n\t\tuser = rule.userId === null ? null : users.findById(rule.userId);\n\t\tif (!user) {\n\t\t\treturn { fire: false, userId: null, reason: `employee ${rule.userId ?? \"?\"} does not exist` };\n\t\t}\n\t}\n\n\tif (!user.isActive) {\n\t\treturn { fire: false, userId: user.id, reason: `${user.displayName} is deactivated` };\n\t}\n\n\treturn { fire: true, userId: user.id, reason: `${rule.action} for ${user.displayName}` };\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4CO,SAAS,YAAY,OAAoC;AAC/D,SAAO,UAAU,OAAO,KAAK,OAAO,KAAK,EAAE,KAAK;AACjD;AAYO,SAAS,kBAAkB,WAA+C;AAChF,QAAM,cAAc,gCAAa,IAAI,KAAK,EAAE,YAAY;AACxD,SAAO,eAAe,YAAY,eAAe;AAClD;AASO,SAAS,mBAAmB,OAA4B,OAAwC;AAtEvG;AAuEC,QAAM,OAAO,YAAY,KAAK;AAC9B,MAAI,CAAC,MAAM;AACV,WAAO;AAAA,EACR;AAEA,MAAI,QAAQ,KAAK,IAAI,GAAG;AACvB,UAAM,OAAO,MAAM,SAAS,OAAO,IAAI,CAAC;AACxC,QAAI,MAAM;AACT,aAAO;AAAA,IACR;AAAA,EACD;AAEA,QAAM,UAAU,MAAM,YAAY,IAAI;AACtC,MAAI,SAAS;AACZ,WAAO;AAAA,EACR;AAEA,QAAM,QAAQ,KAAK,YAAY;AAC/B,UAAO,WAAM,KAAK,EAAE,KAAK,UAAQ,KAAK,YAAY,YAAY,MAAM,KAAK,MAAlE,YAAuE;AAC/E;AAUO,SAAS,gBACf,MACA,SACA,OACkB;AAxGnB;AAyGC,QAAM,OAAO,YAAY,QAAQ,KAAK;AACtC,MAAI,CAAC,MAAM;AACV,WAAO,EAAE,MAAM,OAAO,QAAQ,MAAM,QAAQ,cAAc;AAAA,EAC3D;AACA,MAAI,QAAQ,aAAa,QAAQ,QAAQ,aAAa,MAAM;AAC3D,WAAO,EAAE,MAAM,OAAO,QAAQ,MAAM,QAAQ,SAAS,IAAI,kBAAkB;AAAA,EAC5E;AACA,MAAI,KAAK,cAAc,KAAK,KAAK,gBAAgB,QAAQ,KAAK,cAAc,KAAK,cAAc,QAAQ,KAAK;AAC3G,WAAO,EAAE,MAAM,OAAO,QAAQ,MAAM,QAAQ,eAAe,KAAK,WAAW,sBAAsB;AAAA,EAClG;AAEA,MAAI,OAA0B;AAC9B,MAAI,KAAK,SAAS,QAAQ;AACzB,WAAO,mBAAmB,QAAQ,OAAO,KAAK;AAC9C,QAAI,CAAC,MAAM;AACV,aAAO,EAAE,MAAM,OAAO,QAAQ,MAAM,QAAQ,wBAAwB,IAAI,IAAI;AAAA,IAC7E;AAAA,EACD,OAAO;AACN,QAAI,CAAC,kBAAkB,KAAK,SAAS,KAAK,WAAU,UAAK,cAAL,YAAkB,KAAK;AAC1E,aAAO,EAAE,MAAM,OAAO,QAAQ,MAAM,QAAQ,SAAS,IAAI,oBAAmB,UAAK,cAAL,YAAkB,GAAG,GAAG;AAAA,IACrG;AACA,WAAO,KAAK,WAAW,OAAO,OAAO,MAAM,SAAS,KAAK,MAAM;AAC/D,QAAI,CAAC,MAAM;AACV,aAAO,EAAE,MAAM,OAAO,QAAQ,MAAM,QAAQ,aAAY,UAAK,WAAL,YAAe,GAAG,kBAAkB;AAAA,IAC7F;AAAA,EACD;AAEA,MAAI,CAAC,KAAK,UAAU;AACnB,WAAO,EAAE,MAAM,OAAO,QAAQ,KAAK,IAAI,QAAQ,GAAG,KAAK,WAAW,kBAAkB;AAAA,EACrF;AAEA,SAAO,EAAE,MAAM,MAAM,QAAQ,KAAK,IAAI,QAAQ,GAAG,KAAK,MAAM,QAAQ,KAAK,WAAW,GAAG;AACxF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var database_exports = {};
|
|
30
|
+
__export(database_exports, {
|
|
31
|
+
currentSchemaVersion: () => currentSchemaVersion,
|
|
32
|
+
migrate: () => migrate,
|
|
33
|
+
openAndMigrate: () => openAndMigrate,
|
|
34
|
+
openDatabase: () => openDatabase
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(database_exports);
|
|
37
|
+
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
38
|
+
var import_migrations = require("./migrations");
|
|
39
|
+
function openDatabase(file) {
|
|
40
|
+
const db = new import_better_sqlite3.default(file);
|
|
41
|
+
db.pragma("journal_mode = WAL");
|
|
42
|
+
db.pragma("foreign_keys = ON");
|
|
43
|
+
db.pragma("busy_timeout = 5000");
|
|
44
|
+
db.pragma("synchronous = NORMAL");
|
|
45
|
+
return db;
|
|
46
|
+
}
|
|
47
|
+
function currentSchemaVersion(db) {
|
|
48
|
+
db.exec(
|
|
49
|
+
`CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
50
|
+
version INTEGER PRIMARY KEY,
|
|
51
|
+
name TEXT NOT NULL,
|
|
52
|
+
applied_at INTEGER NOT NULL
|
|
53
|
+
)`
|
|
54
|
+
);
|
|
55
|
+
const row = db.prepare("SELECT COALESCE(MAX(version), 0) AS version FROM schema_migrations").get();
|
|
56
|
+
return row.version;
|
|
57
|
+
}
|
|
58
|
+
function migrate(db, log) {
|
|
59
|
+
const applied = currentSchemaVersion(db);
|
|
60
|
+
const pending = import_migrations.migrations.filter((migration) => migration.version > applied).sort((a, b) => a.version - b.version);
|
|
61
|
+
for (const migration of pending) {
|
|
62
|
+
const applyOne = db.transaction((m) => {
|
|
63
|
+
var _a;
|
|
64
|
+
if (m.sql) {
|
|
65
|
+
db.exec(m.sql);
|
|
66
|
+
}
|
|
67
|
+
(_a = m.run) == null ? void 0 : _a.call(m, db);
|
|
68
|
+
db.prepare("INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)").run(
|
|
69
|
+
m.version,
|
|
70
|
+
m.name,
|
|
71
|
+
Math.floor(Date.now() / 1e3)
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
applyOne(migration);
|
|
75
|
+
log == null ? void 0 : log(`applied database migration ${migration.version}: ${migration.name}`);
|
|
76
|
+
}
|
|
77
|
+
return pending.length;
|
|
78
|
+
}
|
|
79
|
+
function openAndMigrate(file, log) {
|
|
80
|
+
const db = openDatabase(file);
|
|
81
|
+
try {
|
|
82
|
+
migrate(db, log);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
db.close();
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
return db;
|
|
88
|
+
}
|
|
89
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
90
|
+
0 && (module.exports = {
|
|
91
|
+
currentSchemaVersion,
|
|
92
|
+
migrate,
|
|
93
|
+
openAndMigrate,
|
|
94
|
+
openDatabase
|
|
95
|
+
});
|
|
96
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/db/database.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * SQLite access layer: opening the database, applying migrations.\n *\n * Rules (see internal specification, section 2):\n * - WAL journal mode, foreign keys enforced, busy timeout for concurrent access.\n * - Migrations are append-only and applied in one transaction each.\n */\n\nimport Database from \"better-sqlite3\";\nimport { migrations, type Migration } from \"./migrations\";\n\nexport type Db = Database.Database;\n\n/**\n * Opens (or creates) the database file and applies the required pragmas.\n *\n * @param file - database file path or `:memory:`\n * @returns open database handle\n */\nexport function openDatabase(file: string): Db {\n\tconst db = new Database(file);\n\t// WAL keeps readers (PWA/API) working while the adapter writes; ignored for :memory:\n\tdb.pragma(\"journal_mode = WAL\");\n\tdb.pragma(\"foreign_keys = ON\");\n\tdb.pragma(\"busy_timeout = 5000\");\n\tdb.pragma(\"synchronous = NORMAL\");\n\treturn db;\n}\n\n/**\n * Creates the migration bookkeeping table if needed and returns the highest applied version.\n *\n * @param db - open database handle\n * @returns highest applied migration version, 0 when the schema is empty\n */\nexport function currentSchemaVersion(db: Db): number {\n\tdb.exec(\n\t\t`CREATE TABLE IF NOT EXISTS schema_migrations (\n\t\t\tversion INTEGER PRIMARY KEY,\n\t\t\tname TEXT NOT NULL,\n\t\t\tapplied_at INTEGER NOT NULL\n\t\t)`,\n\t);\n\tconst row = db.prepare(\"SELECT COALESCE(MAX(version), 0) AS version FROM schema_migrations\").get() as {\n\t\tversion: number;\n\t};\n\treturn row.version;\n}\n\n/**\n * Applies all pending migrations (ascending) and returns the number of applied migrations.\n *\n * @param db - open database handle\n * @param log - optional log callback (adapter debug log)\n */\nexport function migrate(db: Db, log?: (message: string) => void): number {\n\tconst applied = currentSchemaVersion(db);\n\tconst pending = migrations\n\t\t.filter((migration: Migration) => migration.version > applied)\n\t\t.sort((a: Migration, b: Migration) => a.version - b.version);\n\n\tfor (const migration of pending) {\n\t\tconst applyOne = db.transaction((m: Migration): void => {\n\t\t\t// a migration brings either SQL text or JavaScript steps (or both)\n\t\t\tif (m.sql) {\n\t\t\t\tdb.exec(m.sql);\n\t\t\t}\n\t\t\tm.run?.(db);\n\t\t\tdb.prepare(\"INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)\").run(\n\t\t\t\tm.version,\n\t\t\t\tm.name,\n\t\t\t\tMath.floor(Date.now() / 1000),\n\t\t\t);\n\t\t});\n\t\tapplyOne(migration);\n\t\tlog?.(`applied database migration ${migration.version}: ${migration.name}`);\n\t}\n\n\treturn pending.length;\n}\n\n/**\n * Convenience helper: opens the database, applies migrations and returns the handle.\n *\n * @param file - database file path or `:memory:`\n * @param log - optional callback invoked for every applied migration\n * @returns open database handle with an up-to-date schema\n */\nexport function openAndMigrate(file: string, log?: (message: string) => void): Db {\n\tconst db = openDatabase(file);\n\ttry {\n\t\tmigrate(db, log);\n\t} catch (error) {\n\t\tdb.close();\n\t\tthrow error;\n\t}\n\treturn db;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,4BAAqB;AACrB,wBAA2C;AAUpC,SAAS,aAAa,MAAkB;AAC9C,QAAM,KAAK,IAAI,sBAAAA,QAAS,IAAI;AAE5B,KAAG,OAAO,oBAAoB;AAC9B,KAAG,OAAO,mBAAmB;AAC7B,KAAG,OAAO,qBAAqB;AAC/B,KAAG,OAAO,sBAAsB;AAChC,SAAO;AACR;AAQO,SAAS,qBAAqB,IAAgB;AACpD,KAAG;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAKD;AACA,QAAM,MAAM,GAAG,QAAQ,oEAAoE,EAAE,IAAI;AAGjG,SAAO,IAAI;AACZ;AAQO,SAAS,QAAQ,IAAQ,KAAyC;AACxE,QAAM,UAAU,qBAAqB,EAAE;AACvC,QAAM,UAAU,6BACd,OAAO,CAAC,cAAyB,UAAU,UAAU,OAAO,EAC5D,KAAK,CAAC,GAAc,MAAiB,EAAE,UAAU,EAAE,OAAO;AAE5D,aAAW,aAAa,SAAS;AAChC,UAAM,WAAW,GAAG,YAAY,CAAC,MAAuB;AA9D1D;AAgEG,UAAI,EAAE,KAAK;AACV,WAAG,KAAK,EAAE,GAAG;AAAA,MACd;AACA,cAAE,QAAF,2BAAQ;AACR,SAAG,QAAQ,4EAA4E,EAAE;AAAA,QACxF,EAAE;AAAA,QACF,EAAE;AAAA,QACF,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,MAC7B;AAAA,IACD,CAAC;AACD,aAAS,SAAS;AAClB,+BAAM,8BAA8B,UAAU,OAAO,KAAK,UAAU,IAAI;AAAA,EACzE;AAEA,SAAO,QAAQ;AAChB;AASO,SAAS,eAAe,MAAc,KAAqC;AACjF,QAAM,KAAK,aAAa,IAAI;AAC5B,MAAI;AACH,YAAQ,IAAI,GAAG;AAAA,EAChB,SAAS,OAAO;AACf,OAAG,MAAM;AACT,UAAM;AAAA,EACP;AACA,SAAO;AACR;",
|
|
6
|
+
"names": ["Database"]
|
|
7
|
+
}
|