slotlock 0.7.0 → 0.9.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/CHANGELOG.md +281 -0
- package/agent-server.d.ts +25 -5
- package/agent-server.js +152 -7
- package/agent-store-backend.d.ts +6 -1
- package/agent-store-backend.js +861 -55
- package/application-api.d.ts +19 -1
- package/application-api.js +578 -21
- package/approvals.d.ts +2 -0
- package/approvals.js +12 -5
- package/assistant.d.ts +64 -9
- package/assistant.js +450 -82
- package/availability-store.d.ts +193 -0
- package/availability-store.js +558 -0
- package/booking-rules.d.ts +92 -5
- package/booking-rules.js +243 -15
- package/dashboard-assistant-ui.d.ts +21 -0
- package/dashboard-assistant-ui.js +539 -0
- package/dashboard-markdown.d.ts +2 -0
- package/dashboard-markdown.js +157 -0
- package/dashboard-settings-ui.d.ts +1 -0
- package/dashboard-settings-ui.js +78 -0
- package/dashboard.d.ts +84 -11
- package/dashboard.js +1654 -384
- package/ddl.d.ts +5 -1
- package/ddl.js +760 -9
- package/event-feed.d.ts +1 -1
- package/event-feed.js +18 -0
- package/holiday-import.d.ts +38 -0
- package/holiday-import.js +57 -0
- package/holidays.d.ts +38 -0
- package/holidays.js +150 -0
- package/hours.d.ts +125 -0
- package/hours.js +224 -0
- package/index.d.ts +14 -3
- package/index.js +10 -2
- package/interop.d.ts +25 -7
- package/interop.js +145 -17
- package/lifecycle.d.ts +31 -0
- package/lifecycle.js +115 -0
- package/maintenance.d.ts +19 -0
- package/maintenance.js +55 -1
- package/metrics.js +3 -0
- package/openapi.json +16237 -7795
- package/package.json +1 -1
- package/ranges.d.ts +28 -0
- package/ranges.js +135 -0
- package/resource-search.d.ts +19 -0
- package/resource-search.js +153 -0
- package/rest-api.js +7 -0
- package/rules.d.ts +2 -0
- package/rules.js +2 -0
- package/self-host.d.ts +1 -1
- package/self-host.js +221 -21
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +367 -24
- package/store.js +1567 -97
- package/types.d.ts +198 -0
package/self-host.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// supplies it the same way and nothing is read from a file or a request. A missing or weak secret
|
|
4
4
|
// stops a command before it connects anywhere, and no secret is written to a log line or an error.
|
|
5
5
|
// The one exception is the API key `key create` prints: that output is the key's only copy.
|
|
6
|
-
import { createSlotlockWebhookSender, deliverSlotlockWebhooks, startSlotlockWebhookDelivery, } from './webhooks.js';
|
|
6
|
+
import { createSlotlockGuardedFetch, createSlotlockWebhookSender, deliverSlotlockWebhooks, startSlotlockWebhookDelivery, } from './webhooks.js';
|
|
7
7
|
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
8
8
|
import { parseArgs } from 'node:util';
|
|
9
9
|
import postgres from 'postgres';
|
|
@@ -21,6 +21,7 @@ import { createSlotlockDashboardTeams, createSlotlockOrganizationCheck } from '.
|
|
|
21
21
|
import { SLOTLOCK_DEFAULT_OVERAGE_STOP_PERCENT, reportSlotlockOverage, slotlockMonthOf } from './overage.js';
|
|
22
22
|
import { createSlotlockOtlpExporter, createSlotlockOtlpMetricsExporter, readSlotlockOtlpConfig, } from './otel.js';
|
|
23
23
|
import { readFile } from 'node:fs/promises';
|
|
24
|
+
import { readSlotlockHolidayCalendar } from './holiday-import.js';
|
|
24
25
|
import { SLOTLOCK_DASHBOARD_FUNCTIONS, SLOTLOCK_QUOTA_FUNCTIONS, SLOTLOCK_TENANT_TABLES, SLOTLOCK_USAGE_FUNCTIONS, } from './ddl.js';
|
|
25
26
|
import { maintainSlotlockTenant, startSlotlockMaintenance, withSlotlockMaintenanceLock, } from './maintenance.js';
|
|
26
27
|
import { createSlotlockNodeServer, } from './node-server.js';
|
|
@@ -114,7 +115,9 @@ const SERVED_OPERATIONS = new Set([
|
|
|
114
115
|
'slotlock_get_free_busy',
|
|
115
116
|
'slotlock_find_next_available',
|
|
116
117
|
'slotlock_find_available_slots',
|
|
118
|
+
'slotlock_find_available_ranges',
|
|
117
119
|
'slotlock_hold_slots',
|
|
120
|
+
'slotlock_hold_together',
|
|
118
121
|
'slotlock_release_slots',
|
|
119
122
|
'slotlock_book_together',
|
|
120
123
|
'slotlock_book_pool',
|
|
@@ -929,6 +932,8 @@ export async function startSlotlockServer(config, log) {
|
|
|
929
932
|
maxTenantResources: SERVE_MAX_TENANT_RESOURCES,
|
|
930
933
|
// A granted approval's booking is made as an agent's would be (RFC 0004 A4).
|
|
931
934
|
agentBackend,
|
|
935
|
+
// Holiday calendars by URL (RFC 0004 B1), never from a private address.
|
|
936
|
+
holidayFetch: createSlotlockGuardedFetch(),
|
|
932
937
|
partners: createSlotlockPartnerStore(sql, {
|
|
933
938
|
keys: createSlotlockApiKeyStore(sql),
|
|
934
939
|
eraseTenant: (tenantRef) => store.eraseTenant({ tenantRef }),
|
|
@@ -975,7 +980,8 @@ export async function startSlotlockServer(config, log) {
|
|
|
975
980
|
: {}),
|
|
976
981
|
...(config.dashboard.organizations === 'enabled' ? { teams: createSlotlockDashboardTeams(sql) } : {}),
|
|
977
982
|
keys: createSlotlockApiKeyStore(sql),
|
|
978
|
-
|
|
983
|
+
// Holiday calendars by address (RFC 0004 B1), never from a private address.
|
|
984
|
+
resources: createSlotlockDashboardResources(store, { agentBackend, holidayFetch: createSlotlockGuardedFetch() }),
|
|
979
985
|
state: createSlotlockDashboardState(sql),
|
|
980
986
|
availability: config.availability,
|
|
981
987
|
...(config.usage ? { usage: true } : {}),
|
|
@@ -1037,6 +1043,8 @@ export async function startSlotlockServer(config, log) {
|
|
|
1037
1043
|
SELECT slotlock.maintenance_tenants(${after}::text) AS tenant`),
|
|
1038
1044
|
intervalMs: SERVE_MAINTENANCE_INTERVAL_MS,
|
|
1039
1045
|
...(config.usage ? { usage: true } : {}),
|
|
1046
|
+
// Holiday calendar subscriptions (RFC 0004 B1), never from a private address.
|
|
1047
|
+
holidayFetch: createSlotlockGuardedFetch(),
|
|
1040
1048
|
// Counts only, never the tenant.
|
|
1041
1049
|
onPass: (result) => {
|
|
1042
1050
|
metrics?.maintenance(result);
|
|
@@ -1250,8 +1258,116 @@ function hoursLine(resource) {
|
|
|
1250
1258
|
external_ref: resource.externalRef,
|
|
1251
1259
|
timezone: resource.timezone,
|
|
1252
1260
|
hours: resource.availabilityRules ?? null,
|
|
1261
|
+
inherited: resource.inheritedAvailability ?? null,
|
|
1253
1262
|
})}\n`;
|
|
1254
1263
|
}
|
|
1264
|
+
/**
|
|
1265
|
+
* `closures list|add|delete|import` (RFC 0004 B1): the tenant's closures and special days. Exit 0, 1
|
|
1266
|
+
* when a name, id or file is not found or refused, as JSON lines on stdout.
|
|
1267
|
+
*/
|
|
1268
|
+
async function runClosures(config, io, subcommand, operands, given) {
|
|
1269
|
+
const sql = connect(config.databaseUrl, 1);
|
|
1270
|
+
const fail = (message) => {
|
|
1271
|
+
io.stderr.write(`slotlock: ${message}\n`);
|
|
1272
|
+
return 1;
|
|
1273
|
+
};
|
|
1274
|
+
try {
|
|
1275
|
+
const store = createSlotlockStore(sql);
|
|
1276
|
+
const tenantRef = config.tenantRef;
|
|
1277
|
+
const line = (exception) => io.stdout.write(`${JSON.stringify({
|
|
1278
|
+
id: exception.id,
|
|
1279
|
+
location: exception.location ?? null,
|
|
1280
|
+
resource_id: exception.resourceId ?? null,
|
|
1281
|
+
starts_on: exception.startsOn,
|
|
1282
|
+
ends_on: exception.endsOn,
|
|
1283
|
+
outcome: exception.outcome,
|
|
1284
|
+
hours: exception.hours ?? null,
|
|
1285
|
+
label: exception.label,
|
|
1286
|
+
source: exception.source,
|
|
1287
|
+
})}\n`);
|
|
1288
|
+
let scope = {};
|
|
1289
|
+
if (given.location !== undefined)
|
|
1290
|
+
scope = { location: given.location };
|
|
1291
|
+
if (given.resource !== undefined) {
|
|
1292
|
+
const resources = await tenantResources(store, tenantRef);
|
|
1293
|
+
const named = resources.find((resource) => resource.id === given.resource) ??
|
|
1294
|
+
resources.find((resource) => resource.externalRef === given.resource);
|
|
1295
|
+
if (!named)
|
|
1296
|
+
return fail('no resource with that reference or id in this tenant');
|
|
1297
|
+
scope = { resourceId: named.id };
|
|
1298
|
+
}
|
|
1299
|
+
try {
|
|
1300
|
+
if (subcommand === 'closures list') {
|
|
1301
|
+
const exceptions = await store.withTenant(tenantRef, (tenant) => tenant.listAvailabilityExceptions({
|
|
1302
|
+
tenantRef,
|
|
1303
|
+
from: new Date(Date.now() - 86_400_000).toISOString().slice(0, 10),
|
|
1304
|
+
limit: 1_000,
|
|
1305
|
+
}));
|
|
1306
|
+
for (const exception of exceptions)
|
|
1307
|
+
line(exception);
|
|
1308
|
+
return 0;
|
|
1309
|
+
}
|
|
1310
|
+
if (subcommand === 'closures delete') {
|
|
1311
|
+
const deleted = await store.withTenant(tenantRef, (tenant) => tenant.deleteAvailabilityException({ tenantRef, id: operands[0] }));
|
|
1312
|
+
if (!deleted)
|
|
1313
|
+
return fail('no closure with that id in this tenant');
|
|
1314
|
+
io.stdout.write(`${JSON.stringify({ deleted: true })}\n`);
|
|
1315
|
+
return 0;
|
|
1316
|
+
}
|
|
1317
|
+
if (subcommand === 'closures add') {
|
|
1318
|
+
let hours;
|
|
1319
|
+
if (given.open !== undefined) {
|
|
1320
|
+
const match = /^(\d{2}):(\d{2})-(\d{2}):(\d{2})$/.exec(given.open);
|
|
1321
|
+
if (!match)
|
|
1322
|
+
return fail('--open takes HH:MM-HH:MM, like 10:00-14:00');
|
|
1323
|
+
const [opens, closes] = [Number(match[1]) * 60 + Number(match[2]), Number(match[3]) * 60 + Number(match[4])];
|
|
1324
|
+
hours = [{ startMinutes: opens, durationMinutes: closes > opens ? closes - opens : closes + 1_440 - opens }];
|
|
1325
|
+
}
|
|
1326
|
+
const exception = await store.withTenant(tenantRef, (tenant) => tenant.createAvailabilityException({
|
|
1327
|
+
tenantRef,
|
|
1328
|
+
...scope,
|
|
1329
|
+
startsOn: operands[0],
|
|
1330
|
+
...(operands[1] !== undefined ? { endsOn: operands[1] } : {}),
|
|
1331
|
+
outcome: hours ? 'open' : 'closed',
|
|
1332
|
+
...(hours ? { hours } : {}),
|
|
1333
|
+
label: given.label ?? '',
|
|
1334
|
+
}));
|
|
1335
|
+
line(exception);
|
|
1336
|
+
return 0;
|
|
1337
|
+
}
|
|
1338
|
+
// closures import <file> [--label <calendar name>]
|
|
1339
|
+
let text;
|
|
1340
|
+
try {
|
|
1341
|
+
text = await readFile(operands[0], 'utf8');
|
|
1342
|
+
}
|
|
1343
|
+
catch {
|
|
1344
|
+
return fail('that file cannot be read');
|
|
1345
|
+
}
|
|
1346
|
+
const read = await readSlotlockHolidayCalendar({
|
|
1347
|
+
icalendar: text,
|
|
1348
|
+
...(given.label !== undefined ? { name: given.label } : {}),
|
|
1349
|
+
});
|
|
1350
|
+
const result = await store.withTenant(tenantRef, (tenant) => tenant.importHolidayCalendar({ tenantRef, ...scope, name: read.name, holidays: read.holidays, span: read.span }));
|
|
1351
|
+
io.stdout.write(`${JSON.stringify({ imported: result.imported, removed: result.removed, skipped: read.skipped, calendar: result.calendar.name })}\n`);
|
|
1352
|
+
return 0;
|
|
1353
|
+
}
|
|
1354
|
+
catch (error) {
|
|
1355
|
+
const code = error.code;
|
|
1356
|
+
if (code === 'invalid_availability_exception' ||
|
|
1357
|
+
code === 'invalid_holiday_calendar' ||
|
|
1358
|
+
code === 'location_not_found' ||
|
|
1359
|
+
code === 'resource_not_found' ||
|
|
1360
|
+
code === 'availability_exceptions_limit_reached' ||
|
|
1361
|
+
code === 'holiday_calendars_limit_reached') {
|
|
1362
|
+
return fail(error.message);
|
|
1363
|
+
}
|
|
1364
|
+
throw error;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
finally {
|
|
1368
|
+
await sql.end({ timeout: 5 });
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1255
1371
|
/** Every resource of the tenant in use (with `includeArchived`, every one), a page at a time. */
|
|
1256
1372
|
async function tenantResources(store, tenantRef, includeArchived = false) {
|
|
1257
1373
|
const all = [];
|
|
@@ -1273,14 +1389,22 @@ async function tenantResources(store, tenantRef, includeArchived = false) {
|
|
|
1273
1389
|
* `hours show|set|clear`: print, set or clear (`rules` null) the hours of the tenant's resource
|
|
1274
1390
|
* named by reference or id, or of every resource. Returns false when no resource has that name.
|
|
1275
1391
|
*/
|
|
1276
|
-
async function runHours(config, out, target, rules) {
|
|
1392
|
+
async function runHours(config, out, target, rules, override = 'none') {
|
|
1277
1393
|
const sql = connect(config.databaseUrl, 1);
|
|
1278
1394
|
try {
|
|
1279
1395
|
const store = createSlotlockStore(sql);
|
|
1280
1396
|
const tenantRef = config.tenantRef;
|
|
1281
1397
|
if (target === 'all') {
|
|
1282
|
-
|
|
1283
|
-
|
|
1398
|
+
// The tenant's hours, which every resource without its own inherits (RFC 0004 B2).
|
|
1399
|
+
const result = await store.withTenant(tenantRef, (tenant) => tenant.storeTenantAvailability({ tenantRef, rules: rules ?? null, overrideResources: override }));
|
|
1400
|
+
out.write(`${JSON.stringify({ inheriting: result.inheriting, own_hours: result.ownHours })}\n`);
|
|
1401
|
+
return true;
|
|
1402
|
+
}
|
|
1403
|
+
if (typeof target === 'object' && 'location' in target) {
|
|
1404
|
+
const result = await store.withTenant(tenantRef, (tenant) => tenant.setLocationAvailability({ tenantRef, location: target.location, rules: rules ?? null }));
|
|
1405
|
+
if (!result)
|
|
1406
|
+
return false;
|
|
1407
|
+
out.write(`${JSON.stringify({ location: target.location, inheriting: result.inheriting, own_hours: result.ownHours })}\n`);
|
|
1284
1408
|
return true;
|
|
1285
1409
|
}
|
|
1286
1410
|
const resources = await tenantResources(store, tenantRef);
|
|
@@ -1298,6 +1422,8 @@ async function runHours(config, out, target, rules) {
|
|
|
1298
1422
|
out.write(hoursLine(named));
|
|
1299
1423
|
return true;
|
|
1300
1424
|
}
|
|
1425
|
+
if (!('resource' in target))
|
|
1426
|
+
return false;
|
|
1301
1427
|
const updated = await store.withTenant(tenantRef, (tenant) => tenant.setResourceAvailability({ tenantRef, id: named.id, rules }));
|
|
1302
1428
|
if (!updated)
|
|
1303
1429
|
return false;
|
|
@@ -1780,11 +1906,16 @@ async function maintainTenant(config, caps) {
|
|
|
1780
1906
|
noShows: 0,
|
|
1781
1907
|
waitlistOffers: 0,
|
|
1782
1908
|
approvalsExpired: 0,
|
|
1909
|
+
bookingsCompleted: 0,
|
|
1910
|
+
remindersDue: 0,
|
|
1911
|
+
holidayCalendarsImported: 0,
|
|
1783
1912
|
capped: true,
|
|
1784
1913
|
passes: 0,
|
|
1785
1914
|
};
|
|
1915
|
+
// Holiday calendar subscriptions by URL (RFC 0004 B1), never from a private address.
|
|
1916
|
+
const holidayFetch = createSlotlockGuardedFetch();
|
|
1786
1917
|
while (sum.capped && sum.passes < MAINTAIN_MAX_PASSES) {
|
|
1787
|
-
const pass = await maintainSlotlockTenant(store, config.tenantRef);
|
|
1918
|
+
const pass = await maintainSlotlockTenant(store, config.tenantRef, { holidayFetch });
|
|
1788
1919
|
sum.passes += 1;
|
|
1789
1920
|
sum.extended += pass.extended;
|
|
1790
1921
|
sum.conflicts += pass.conflicts;
|
|
@@ -1795,6 +1926,9 @@ async function maintainTenant(config, caps) {
|
|
|
1795
1926
|
sum.noShows += pass.noShows;
|
|
1796
1927
|
sum.waitlistOffers += pass.waitlistOffers;
|
|
1797
1928
|
sum.approvalsExpired += pass.approvalsExpired;
|
|
1929
|
+
sum.bookingsCompleted += pass.bookingsCompleted;
|
|
1930
|
+
sum.remindersDue += pass.remindersDue;
|
|
1931
|
+
sum.holidayCalendarsImported += pass.holidayCalendarsImported;
|
|
1798
1932
|
// A capped pass that got nowhere met only series it cannot extend. While its roll stopped
|
|
1799
1933
|
// before the series it had put back, the next pass meets new ones (and perhaps a healthy
|
|
1800
1934
|
// one); once it reached them, another pass would meet them again.
|
|
@@ -1872,11 +2006,26 @@ Commands:
|
|
|
1872
2006
|
resource delete <ref> Delete a resource and its history; refused while a booking, hold
|
|
1873
2007
|
or event on it has not ended
|
|
1874
2008
|
hours show [--resource <ref>]
|
|
1875
|
-
Print each resource's bookable hours (null
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2009
|
+
Print each resource's bookable hours: its own (null when none) and
|
|
2010
|
+
what it inherits from its location or the tenant
|
|
2011
|
+
hours set <json> (--resource <ref> | --location <ref> | --all [--override | --matching])
|
|
2012
|
+
Give one resource, one location or the tenant weekly hours; a
|
|
2013
|
+
resource without its own inherits its location's, then the
|
|
2014
|
+
tenant's, then the server's; --override also clears resources'
|
|
2015
|
+
own hours, --matching only those the same as the tenant's were
|
|
2016
|
+
or as these (copies 0.8.0 made); [] closes
|
|
2017
|
+
hours clear (--resource <ref> | --location <ref> | --all)
|
|
2018
|
+
Clear those hours, so they inherit from further up
|
|
2019
|
+
closures list Print the tenant's closures and special days from yesterday on
|
|
2020
|
+
closures add <first-day> [<last-day>] --label <text> [--open HH:MM-HH:MM]
|
|
2021
|
+
[--resource <ref> | --location <ref>]
|
|
2022
|
+
Close whole days (YYYY-MM-DD), or with --open give them those hours
|
|
2023
|
+
instead, everywhere or for one location or resource
|
|
2024
|
+
closures delete <id> Remove a closure
|
|
2025
|
+
closures import <file> [--resource <ref> | --location <ref>] [--label <calendar name>]
|
|
2026
|
+
Close the days an iCalendar file of holidays names (each all-day
|
|
2027
|
+
event, for the next two years); importing the calendar again (by its
|
|
2028
|
+
name: --label, else its own) updates them and removes those it dropped
|
|
1880
2029
|
rules set <json> --resource <ref>
|
|
1881
2030
|
Set what a booking on the resource must satisfy, like
|
|
1882
2031
|
{"minNoticeMinutes":120,"slotStepMinutes":30,"turnaroundMinutes":30,"enforceHours":true}
|
|
@@ -2096,6 +2245,7 @@ export async function runSlotlockCli(argv, io) {
|
|
|
2096
2245
|
const subcommand = command === 'resource' ||
|
|
2097
2246
|
command === 'key' ||
|
|
2098
2247
|
command === 'hours' ||
|
|
2248
|
+
command === 'closures' ||
|
|
2099
2249
|
command === 'rules' ||
|
|
2100
2250
|
command === 'tenant' ||
|
|
2101
2251
|
command === 'plan' ||
|
|
@@ -2134,6 +2284,10 @@ export async function runSlotlockCli(argv, io) {
|
|
|
2134
2284
|
'hours show': 1,
|
|
2135
2285
|
'hours set': 2,
|
|
2136
2286
|
'hours clear': 1,
|
|
2287
|
+
'closures list': 1,
|
|
2288
|
+
'closures add': 2,
|
|
2289
|
+
'closures delete': 2,
|
|
2290
|
+
'closures import': 2,
|
|
2137
2291
|
'rules set': 2,
|
|
2138
2292
|
'rules clear': 1,
|
|
2139
2293
|
'partner create': 3,
|
|
@@ -2148,7 +2302,8 @@ export async function runSlotlockCli(argv, io) {
|
|
|
2148
2302
|
'overage report': 1,
|
|
2149
2303
|
'billing daily': 1,
|
|
2150
2304
|
};
|
|
2151
|
-
|
|
2305
|
+
// closures add takes a last day or not.
|
|
2306
|
+
const expected = subcommand === 'closures add' && operands.length === 3 ? 3 : expectedOperands[subcommand];
|
|
2152
2307
|
if (expected === undefined)
|
|
2153
2308
|
return usage(`unknown command "${subcommand}"`);
|
|
2154
2309
|
if (operands.length !== expected)
|
|
@@ -2208,10 +2363,32 @@ export async function runSlotlockCli(argv, io) {
|
|
|
2208
2363
|
if (values.all && subcommand !== 'hours set' && subcommand !== 'hours clear') {
|
|
2209
2364
|
return usage('--all applies only to hours set and hours clear');
|
|
2210
2365
|
}
|
|
2366
|
+
if (values.override && subcommand !== 'hours set')
|
|
2367
|
+
return usage('--override applies only to hours set --all');
|
|
2368
|
+
if (values.matching && subcommand !== 'hours set')
|
|
2369
|
+
return usage('--matching applies only to hours set --all');
|
|
2370
|
+
if (values.override && values.matching)
|
|
2371
|
+
return usage('--override and --matching: give one');
|
|
2211
2372
|
if (values.resource !== undefined &&
|
|
2212
2373
|
!subcommand.startsWith('hours ') &&
|
|
2213
|
-
!subcommand.startsWith('rules ')
|
|
2214
|
-
|
|
2374
|
+
!subcommand.startsWith('rules ') &&
|
|
2375
|
+
!subcommand.startsWith('closures ')) {
|
|
2376
|
+
return usage('--resource applies only to hours, closures and rules commands');
|
|
2377
|
+
}
|
|
2378
|
+
if (values.location !== undefined && !subcommand.startsWith('hours ') && !subcommand.startsWith('closures ')) {
|
|
2379
|
+
return usage('--location applies only to hours and closures commands');
|
|
2380
|
+
}
|
|
2381
|
+
if (values.label !== undefined && subcommand !== 'closures add' && subcommand !== 'closures import') {
|
|
2382
|
+
return usage('--label applies only to closures add and closures import');
|
|
2383
|
+
}
|
|
2384
|
+
if (values.open !== undefined && subcommand !== 'closures add') {
|
|
2385
|
+
return usage('--open applies only to closures add');
|
|
2386
|
+
}
|
|
2387
|
+
if (subcommand.startsWith('closures ') && values.resource !== undefined && values.location !== undefined) {
|
|
2388
|
+
return usage(`${subcommand} takes --resource or --location, not both`);
|
|
2389
|
+
}
|
|
2390
|
+
if (subcommand === 'closures add' && (operands.length < 2 || operands.length > 3 || values.label === undefined)) {
|
|
2391
|
+
return usage('closures add needs <first-day> [<last-day>] --label <text>');
|
|
2215
2392
|
}
|
|
2216
2393
|
if (subcommand.startsWith('rules ') && values.resource === undefined) {
|
|
2217
2394
|
return usage(`${subcommand} needs --resource <ref>`);
|
|
@@ -2228,9 +2405,13 @@ export async function runSlotlockCli(argv, io) {
|
|
|
2228
2405
|
}
|
|
2229
2406
|
}
|
|
2230
2407
|
if ((subcommand === 'hours set' || subcommand === 'hours clear') &&
|
|
2231
|
-
|
|
2232
|
-
return usage(`${subcommand} needs --resource <ref> or --all, one of them`);
|
|
2408
|
+
[values.resource !== undefined, values.location !== undefined, values.all === true].filter(Boolean).length !== 1) {
|
|
2409
|
+
return usage(`${subcommand} needs --resource <ref>, --location <ref> or --all, one of them`);
|
|
2233
2410
|
}
|
|
2411
|
+
if (values.override && values.all !== true)
|
|
2412
|
+
return usage('--override applies only to hours set --all');
|
|
2413
|
+
if (values.matching && values.all !== true)
|
|
2414
|
+
return usage('--matching applies only to hours set --all');
|
|
2234
2415
|
let hours = null;
|
|
2235
2416
|
if (subcommand === 'hours set') {
|
|
2236
2417
|
let candidate;
|
|
@@ -2484,16 +2665,30 @@ export async function runSlotlockCli(argv, io) {
|
|
|
2484
2665
|
case 'hours clear': {
|
|
2485
2666
|
const target = values.all === true
|
|
2486
2667
|
? 'all'
|
|
2487
|
-
: values.
|
|
2488
|
-
? {
|
|
2489
|
-
:
|
|
2490
|
-
|
|
2668
|
+
: values.location !== undefined
|
|
2669
|
+
? { location: values.location }
|
|
2670
|
+
: values.resource !== undefined
|
|
2671
|
+
? { resource: values.resource }
|
|
2672
|
+
: 'show';
|
|
2673
|
+
const found = await runHours(readSlotlockDatabaseConfig(io.env), io.stdout, target, subcommand === 'hours show' ? undefined : hours, values.override === true ? 'all' : values.matching === true ? 'matching' : 'none');
|
|
2491
2674
|
if (!found) {
|
|
2492
|
-
io.stderr.write(
|
|
2675
|
+
io.stderr.write(values.location !== undefined
|
|
2676
|
+
? 'slotlock: no location with that reference in this tenant\n'
|
|
2677
|
+
: 'slotlock: no resource with that reference or id in this tenant\n');
|
|
2493
2678
|
return 1;
|
|
2494
2679
|
}
|
|
2495
2680
|
return 0;
|
|
2496
2681
|
}
|
|
2682
|
+
case 'closures list':
|
|
2683
|
+
case 'closures add':
|
|
2684
|
+
case 'closures delete':
|
|
2685
|
+
case 'closures import':
|
|
2686
|
+
return runClosures(readSlotlockDatabaseConfig(io.env), io, subcommand, operands.slice(1), {
|
|
2687
|
+
...(values.resource !== undefined ? { resource: values.resource } : {}),
|
|
2688
|
+
...(values.location !== undefined ? { location: values.location } : {}),
|
|
2689
|
+
...(values.label !== undefined ? { label: values.label } : {}),
|
|
2690
|
+
...(values.open !== undefined ? { open: values.open } : {}),
|
|
2691
|
+
});
|
|
2497
2692
|
case 'rules set':
|
|
2498
2693
|
case 'rules clear': {
|
|
2499
2694
|
const found = await runRules(readSlotlockDatabaseConfig(io.env), io.stdout, values.resource, bookingRules);
|
|
@@ -2594,7 +2789,12 @@ function parseCliArguments(argv) {
|
|
|
2594
2789
|
month: { type: 'string' },
|
|
2595
2790
|
event: { type: 'string' },
|
|
2596
2791
|
resource: { type: 'string' },
|
|
2792
|
+
location: { type: 'string' },
|
|
2597
2793
|
all: { type: 'boolean' },
|
|
2794
|
+
override: { type: 'boolean' },
|
|
2795
|
+
matching: { type: 'boolean' },
|
|
2796
|
+
label: { type: 'string' },
|
|
2797
|
+
open: { type: 'string' },
|
|
2598
2798
|
reason: { type: 'string' },
|
|
2599
2799
|
confirm: { type: 'string' },
|
|
2600
2800
|
from: { type: 'string' },
|
package/slotlock.manifest.json
CHANGED