slotlock 0.5.2 → 0.5.3
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 +98 -0
- package/README.md +97 -18
- package/SPEC.md +10 -7
- package/agent-server.d.ts +38 -4
- package/agent-server.js +464 -145
- package/agent-store-backend.d.ts +9 -0
- package/agent-store-backend.js +83 -15
- package/api-keys.d.ts +12 -2
- package/api-keys.js +36 -7
- package/application-api.d.ts +7 -1
- package/application-api.js +67 -7
- package/assistant.js +2 -1
- package/booking-rules.d.ts +61 -9
- package/booking-rules.js +106 -16
- package/change-watch.d.ts +56 -0
- package/change-watch.js +213 -0
- package/dashboard.d.ts +3 -1
- package/dashboard.js +98 -15
- package/ddl.d.ts +1 -1
- package/ddl.js +395 -4
- package/event-feed.d.ts +37 -4
- package/event-feed.js +74 -6
- package/index.d.ts +7 -6
- package/index.js +5 -4
- package/maintenance.js +12 -10
- package/openapi.json +1284 -82
- package/package.json +1 -1
- package/self-host.js +8 -4
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +32 -3
- package/store.js +324 -124
- package/types.d.ts +14 -1
- package/webhooks.d.ts +9 -2
- package/webhooks.js +6 -1
package/ddl.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// the caller's search_path back at the end, so a caller that keeps going in the same transaction
|
|
12
12
|
// resolves its own names as before. Apply it in one transaction: under autocommit every statement
|
|
13
13
|
// is a transaction of its own, and the pin ends with the statement that sets it.
|
|
14
|
+
import { SLOTLOCK_CHANGE_ORIGINS, slotlockCoverageSourceIdSql } from './event-feed.js';
|
|
14
15
|
import { SLOTLOCK_MAX_AVAILABILITY_RULES } from './weekly-hours.js';
|
|
15
16
|
const CALLER_SEARCH_PATH_SETTING = 'slotlock.caller_search_path';
|
|
16
17
|
const DEPLOYMENT_SEARCH_PATH_PIN = `SELECT pg_catalog.set_config('${CALLER_SEARCH_PATH_SETTING}', pg_catalog.current_setting('search_path'), true);
|
|
@@ -550,6 +551,55 @@ AS $slotlock_erase_api_keys$
|
|
|
550
551
|
SELECT count(*) FROM erased;
|
|
551
552
|
$slotlock_erase_api_keys$;
|
|
552
553
|
REVOKE ALL ON FUNCTION ${SLOTLOCK_API_KEY_FUNCTIONS[5]} FROM PUBLIC;
|
|
554
|
+
|
|
555
|
+
-- A key created, given a new secret or revoked appends to its tenant's feed (key.created,
|
|
556
|
+
-- key.rotated, key.revoked): its id, scopes and expiry, never its digest, prefix or name. Only the
|
|
557
|
+
-- functions above write keys, as the schema's owner, which forced row-level security on the feed
|
|
558
|
+
-- binds too: the row is appended under the tenant's name in every setting a policy reads, and the
|
|
559
|
+
-- caller's values are put back after. Who did it is the caller's actor, else the key's creator.
|
|
560
|
+
CREATE OR REPLACE FUNCTION slotlock.outbox_api_key_change()
|
|
561
|
+
RETURNS trigger
|
|
562
|
+
LANGUAGE plpgsql
|
|
563
|
+
SET search_path = pg_catalog, slotlock, pg_temp
|
|
564
|
+
AS $slotlock_outbox_api_key$
|
|
565
|
+
DECLARE
|
|
566
|
+
kind text;
|
|
567
|
+
setting text;
|
|
568
|
+
previous jsonb := '{}'::jsonb;
|
|
569
|
+
BEGIN
|
|
570
|
+
IF TG_OP = 'INSERT' THEN
|
|
571
|
+
kind := 'key.created';
|
|
572
|
+
ELSIF OLD.revoked_at IS NULL AND NEW.revoked_at IS NOT NULL THEN
|
|
573
|
+
kind := 'key.revoked';
|
|
574
|
+
ELSIF OLD.secret_hash IS DISTINCT FROM NEW.secret_hash THEN
|
|
575
|
+
kind := 'key.rotated';
|
|
576
|
+
ELSE
|
|
577
|
+
RETURN NULL;
|
|
578
|
+
END IF;
|
|
579
|
+
FOR setting IN
|
|
580
|
+
SELECT DISTINCT contract.tenant_context_setting FROM slotlock.rls_policy_contracts contract
|
|
581
|
+
LOOP
|
|
582
|
+
previous := previous || jsonb_build_object(setting, current_setting(setting, true));
|
|
583
|
+
PERFORM set_config(setting, NEW.tenant_ref, true);
|
|
584
|
+
END LOOP;
|
|
585
|
+
INSERT INTO slotlock.outbox (tenant_ref, type, subject, data, actor)
|
|
586
|
+
VALUES (
|
|
587
|
+
NEW.tenant_ref, kind, NEW.id::text,
|
|
588
|
+
jsonb_build_object(
|
|
589
|
+
'id', NEW.id, 'scopes', to_jsonb(NEW.scopes), 'expires_at', NEW.expires_at,
|
|
590
|
+
'revoked_at', NEW.revoked_at),
|
|
591
|
+
COALESCE(NULLIF(current_setting('slotlock.actor', true), ''),
|
|
592
|
+
CASE WHEN TG_OP = 'INSERT' THEN NEW.created_by END)
|
|
593
|
+
);
|
|
594
|
+
FOR setting IN SELECT jsonb_object_keys(previous) LOOP
|
|
595
|
+
PERFORM set_config(setting, COALESCE(previous ->> setting, ''), true);
|
|
596
|
+
END LOOP;
|
|
597
|
+
RETURN NULL;
|
|
598
|
+
END
|
|
599
|
+
$slotlock_outbox_api_key$;
|
|
600
|
+
CREATE OR REPLACE TRIGGER slotlock_outbox_api_key_change
|
|
601
|
+
AFTER INSERT OR UPDATE OF revoked_at, secret_hash ON slotlock.api_keys
|
|
602
|
+
FOR EACH ROW EXECUTE FUNCTION slotlock.outbox_api_key_change();
|
|
553
603
|
`;
|
|
554
604
|
// The dashboard's records (dashboard.ts), shared by every server on this database: a finished sign-in
|
|
555
605
|
// (its callback works once), a sent form (it runs once) and a signed-out session (a copied cookie
|
|
@@ -875,6 +925,10 @@ BEGIN
|
|
|
875
925
|
DELETE FROM slotlock.waitlist_entries WHERE tenant_ref = requested_tenant_ref;
|
|
876
926
|
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
877
927
|
RETURN NEXT;
|
|
928
|
+
erased_table := 'booking_rule_defaults';
|
|
929
|
+
DELETE FROM slotlock.booking_rule_defaults WHERE tenant_ref = requested_tenant_ref;
|
|
930
|
+
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
931
|
+
RETURN NEXT;
|
|
878
932
|
erased_table := 'assistant_conversations';
|
|
879
933
|
DELETE FROM slotlock.assistant_conversations WHERE tenant_ref = requested_tenant_ref;
|
|
880
934
|
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
@@ -1194,6 +1248,36 @@ CREATE TABLE IF NOT EXISTS slotlock.outbox (
|
|
|
1194
1248
|
);
|
|
1195
1249
|
-- Who made the change (withTenant's actor: an API key, an agent), for the history.
|
|
1196
1250
|
ALTER TABLE slotlock.outbox ADD COLUMN IF NOT EXISTS actor text;
|
|
1251
|
+
-- Where it came from (withTenant's origin: mcp, a2a, rest, application, dashboard,
|
|
1252
|
+
-- dashboard-assistant, maintenance, library) and the API key that made it (withTenant's keyId).
|
|
1253
|
+
-- Added bare, so the rows already there keep null (an earlier release) and nothing is scanned under
|
|
1254
|
+
-- the lock; the defaults then bind only new rows. A write that names no origin (code calling the
|
|
1255
|
+
-- store outside withTenant, or SQL of its own) is \`library\`.
|
|
1256
|
+
ALTER TABLE slotlock.outbox ADD COLUMN IF NOT EXISTS origin text;
|
|
1257
|
+
ALTER TABLE slotlock.outbox ADD COLUMN IF NOT EXISTS key_id uuid;
|
|
1258
|
+
ALTER TABLE slotlock.outbox
|
|
1259
|
+
ALTER COLUMN origin SET DEFAULT COALESCE(NULLIF(current_setting('slotlock.origin', true), ''), 'library'),
|
|
1260
|
+
ALTER COLUMN key_id SET DEFAULT NULLIF(current_setting('slotlock.key_id', true), '')::uuid;
|
|
1261
|
+
-- The origins the API reads, and no other. NOT VALID, so adding it scans nothing under the lock;
|
|
1262
|
+
-- DEPLOYMENT_VALIDATE_CONSTRAINTS validates it after the migration commits. Its comment is the list
|
|
1263
|
+
-- it was built from: a release that changes the list replaces it, so an upgraded database never
|
|
1264
|
+
-- keeps refusing an origin the release writes.
|
|
1265
|
+
DO $slotlock_outbox_origin$
|
|
1266
|
+
BEGIN
|
|
1267
|
+
ALTER TABLE slotlock.outbox DROP CONSTRAINT IF EXISTS slotlock_outbox_origin_valid;
|
|
1268
|
+
IF (
|
|
1269
|
+
SELECT pg_catalog.obj_description(oid, 'pg_constraint') FROM pg_catalog.pg_constraint
|
|
1270
|
+
WHERE conrelid = 'slotlock.outbox'::regclass AND conname = 'slotlock_outbox_origin_known'
|
|
1271
|
+
) IS DISTINCT FROM '${SLOTLOCK_CHANGE_ORIGINS.join(' ')}' THEN
|
|
1272
|
+
ALTER TABLE slotlock.outbox DROP CONSTRAINT IF EXISTS slotlock_outbox_origin_known;
|
|
1273
|
+
ALTER TABLE slotlock.outbox
|
|
1274
|
+
ADD CONSTRAINT slotlock_outbox_origin_known
|
|
1275
|
+
CHECK (origin IN (${SLOTLOCK_CHANGE_ORIGINS.map((origin) => `'${origin}'`).join(', ')})) NOT VALID;
|
|
1276
|
+
COMMENT ON CONSTRAINT slotlock_outbox_origin_known ON slotlock.outbox
|
|
1277
|
+
IS '${SLOTLOCK_CHANGE_ORIGINS.join(' ')}';
|
|
1278
|
+
END IF;
|
|
1279
|
+
END
|
|
1280
|
+
$slotlock_outbox_origin$;
|
|
1197
1281
|
CREATE INDEX IF NOT EXISTS slotlock_outbox_tenant_resource_idx
|
|
1198
1282
|
ON slotlock.outbox (tenant_ref, resource_id, id) WHERE resource_id IS NOT NULL;
|
|
1199
1283
|
CREATE INDEX IF NOT EXISTS slotlock_outbox_tenant_subject_idx
|
|
@@ -1248,6 +1332,10 @@ $slotlock_outbox_mark_pruned$;
|
|
|
1248
1332
|
CREATE OR REPLACE TRIGGER slotlock_outbox_mark_pruned
|
|
1249
1333
|
AFTER DELETE ON slotlock.outbox REFERENCING OLD TABLE AS pruned
|
|
1250
1334
|
FOR EACH STATEMENT EXECUTE FUNCTION slotlock.outbox_mark_pruned();
|
|
1335
|
+
-- Live updates poll each tenant's feed (change-watch.ts). A pre-release build woke readers with a
|
|
1336
|
+
-- notification trigger here, which made every outbox commit take the cluster-wide notify lock: drop it.
|
|
1337
|
+
DROP TRIGGER IF EXISTS slotlock_outbox_notify ON slotlock.outbox;
|
|
1338
|
+
DROP FUNCTION IF EXISTS slotlock.outbox_notify();
|
|
1251
1339
|
|
|
1252
1340
|
CREATE TABLE IF NOT EXISTS slotlock.webhook_endpoints (
|
|
1253
1341
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
@@ -1275,14 +1363,31 @@ CREATE TABLE IF NOT EXISTS slotlock.webhook_endpoints (
|
|
|
1275
1363
|
AND octet_length(url) BETWEEN 1 AND 2048
|
|
1276
1364
|
AND secret ~ '^whsec_[A-Za-z0-9+/]{43}=$'
|
|
1277
1365
|
),
|
|
1278
|
-
CONSTRAINT
|
|
1279
|
-
event_types IS NULL OR cardinality(event_types) BETWEEN 1 AND
|
|
1366
|
+
CONSTRAINT slotlock_webhook_endpoints_type_count_valid CHECK (
|
|
1367
|
+
event_types IS NULL OR cardinality(event_types) BETWEEN 1 AND 64
|
|
1280
1368
|
),
|
|
1281
1369
|
CONSTRAINT slotlock_webhook_endpoints_error_valid CHECK (
|
|
1282
1370
|
last_error IS NULL OR char_length(last_error) <= 500
|
|
1283
1371
|
)
|
|
1284
1372
|
);
|
|
1285
1373
|
ALTER TABLE slotlock.webhook_endpoints ADD COLUMN IF NOT EXISTS changes_pruned boolean NOT NULL DEFAULT false;
|
|
1374
|
+
-- 0.5 allowed a filter of at most 20 types, fewer than there are now. The wider CHECK admits every
|
|
1375
|
+
-- row the old one did, so it is added NOT VALID (no scan under the lock), and the old one dropped.
|
|
1376
|
+
DO $slotlock_webhook_type_count$
|
|
1377
|
+
BEGIN
|
|
1378
|
+
IF NOT EXISTS (
|
|
1379
|
+
SELECT 1 FROM pg_catalog.pg_constraint
|
|
1380
|
+
WHERE conrelid = 'slotlock.webhook_endpoints'::regclass
|
|
1381
|
+
AND conname = 'slotlock_webhook_endpoints_type_count_valid'
|
|
1382
|
+
) THEN
|
|
1383
|
+
ALTER TABLE slotlock.webhook_endpoints
|
|
1384
|
+
ADD CONSTRAINT slotlock_webhook_endpoints_type_count_valid CHECK (
|
|
1385
|
+
event_types IS NULL OR cardinality(event_types) BETWEEN 1 AND 64
|
|
1386
|
+
) NOT VALID;
|
|
1387
|
+
END IF;
|
|
1388
|
+
ALTER TABLE slotlock.webhook_endpoints DROP CONSTRAINT IF EXISTS slotlock_webhook_endpoints_types_valid;
|
|
1389
|
+
END
|
|
1390
|
+
$slotlock_webhook_type_count$;
|
|
1286
1391
|
CREATE INDEX IF NOT EXISTS slotlock_webhook_endpoints_tenant_idx
|
|
1287
1392
|
ON slotlock.webhook_endpoints (tenant_ref, created_at);
|
|
1288
1393
|
|
|
@@ -1321,6 +1426,23 @@ CREATE INDEX IF NOT EXISTS slotlock_waitlist_entries_queue_idx
|
|
|
1321
1426
|
CREATE INDEX IF NOT EXISTS slotlock_waitlist_entries_tenant_idx
|
|
1322
1427
|
ON slotlock.waitlist_entries (tenant_ref, starts_at);
|
|
1323
1428
|
|
|
1429
|
+
-- Default booking rules (booking-rules.ts), for the tenant (pool_ref '') or for one of its pools:
|
|
1430
|
+
-- a resource's rules are the tenant's, overlaid by its pool's, overlaid by its own.
|
|
1431
|
+
CREATE TABLE IF NOT EXISTS slotlock.booking_rule_defaults (
|
|
1432
|
+
tenant_ref text NOT NULL,
|
|
1433
|
+
pool_ref text NOT NULL DEFAULT '',
|
|
1434
|
+
rules jsonb NOT NULL,
|
|
1435
|
+
updated_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
|
1436
|
+
CONSTRAINT slotlock_booking_rule_defaults_pkey PRIMARY KEY (tenant_ref, pool_ref),
|
|
1437
|
+
CONSTRAINT slotlock_booking_rule_defaults_identity_valid CHECK (
|
|
1438
|
+
octet_length(tenant_ref) BETWEEN 1 AND 500
|
|
1439
|
+
AND (pool_ref = '' OR pool_ref ~ '^[a-z0-9][a-z0-9._-]{0,99}$')
|
|
1440
|
+
),
|
|
1441
|
+
CONSTRAINT slotlock_booking_rule_defaults_rules_valid CHECK (
|
|
1442
|
+
jsonb_typeof(rules) = 'object' AND octet_length(rules::text) <= 4096
|
|
1443
|
+
)
|
|
1444
|
+
);
|
|
1445
|
+
|
|
1324
1446
|
-- The model a tenant's dashboard assistant uses (dashboard.ts, assistant.ts), one row a tenant.
|
|
1325
1447
|
-- The API key is stored only sealed (AES-256-GCM under the deployment's SLOTLOCK_ASSISTANT_SECRET,
|
|
1326
1448
|
-- bound to the tenant), with its last four characters to show.
|
|
@@ -1543,6 +1665,163 @@ ALTER TABLE slotlock.calendar_coverage ADD COLUMN IF NOT EXISTS last_error text;
|
|
|
1543
1665
|
ALTER TABLE slotlock.calendar_coverage ADD COLUMN IF NOT EXISTS last_error_at timestamptz;
|
|
1544
1666
|
ALTER TABLE slotlock.calendar_coverage ADD COLUMN IF NOT EXISTS stale_reported_at timestamptz;
|
|
1545
1667
|
|
|
1668
|
+
-- Configuration changes (RFC 0002 P1-6): a resource's own settings, the sources its coverage is
|
|
1669
|
+
-- read from, and the tenant's webhook endpoints. Each change that alters something appends one row,
|
|
1670
|
+
-- with what it was before and after for the fields it changed; writing the same value again appends
|
|
1671
|
+
-- none. Erasure is not reported, as above.
|
|
1672
|
+
CREATE OR REPLACE FUNCTION slotlock.outbox_erasing(owner text)
|
|
1673
|
+
RETURNS boolean
|
|
1674
|
+
LANGUAGE sql
|
|
1675
|
+
STABLE
|
|
1676
|
+
SET search_path = pg_catalog, slotlock, pg_temp
|
|
1677
|
+
AS $slotlock_outbox_erasing$
|
|
1678
|
+
SELECT owner = current_setting('slotlock.erasing', true)
|
|
1679
|
+
AND current_user = (SELECT pg_catalog.pg_get_userbyid(nspowner) FROM pg_catalog.pg_namespace
|
|
1680
|
+
WHERE nspname = 'slotlock');
|
|
1681
|
+
$slotlock_outbox_erasing$;
|
|
1682
|
+
|
|
1683
|
+
CREATE OR REPLACE FUNCTION slotlock.outbox_resource_change()
|
|
1684
|
+
RETURNS trigger
|
|
1685
|
+
LANGUAGE plpgsql
|
|
1686
|
+
SET search_path = pg_catalog, slotlock, pg_temp
|
|
1687
|
+
AS $slotlock_outbox_resource$
|
|
1688
|
+
DECLARE
|
|
1689
|
+
changed slotlock.resources%ROWTYPE;
|
|
1690
|
+
kind text;
|
|
1691
|
+
before jsonb := '{}'::jsonb;
|
|
1692
|
+
after jsonb := '{}'::jsonb;
|
|
1693
|
+
details jsonb;
|
|
1694
|
+
BEGIN
|
|
1695
|
+
changed := CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
|
|
1696
|
+
IF changed.tenant_ref IS NULL OR slotlock.outbox_erasing(changed.tenant_ref) THEN RETURN NULL; END IF;
|
|
1697
|
+
IF TG_OP = 'INSERT' THEN
|
|
1698
|
+
kind := 'resource.created';
|
|
1699
|
+
details := jsonb_build_object(
|
|
1700
|
+
'timezone', NEW.timezone, 'visibility', NEW.visibility, 'pool', NEW.pool_ref,
|
|
1701
|
+
'availability', NEW.availability_rules, 'booking_rules', NEW.booking_rules);
|
|
1702
|
+
ELSIF TG_OP = 'DELETE' THEN
|
|
1703
|
+
kind := 'resource.deleted';
|
|
1704
|
+
details := '{}'::jsonb;
|
|
1705
|
+
ELSIF (OLD.archived_at IS NULL) <> (NEW.archived_at IS NULL) THEN
|
|
1706
|
+
kind := CASE WHEN NEW.archived_at IS NULL THEN 'resource.restored' ELSE 'resource.archived' END;
|
|
1707
|
+
details := jsonb_build_object('archived_at', NEW.archived_at);
|
|
1708
|
+
ELSE
|
|
1709
|
+
-- Only what changed, under the names the API gives it; a null is kept (no pool, no rules).
|
|
1710
|
+
IF OLD.external_ref IS DISTINCT FROM NEW.external_ref THEN
|
|
1711
|
+
before := before || jsonb_build_object('reference', OLD.external_ref);
|
|
1712
|
+
after := after || jsonb_build_object('reference', NEW.external_ref);
|
|
1713
|
+
END IF;
|
|
1714
|
+
IF OLD.timezone IS DISTINCT FROM NEW.timezone THEN
|
|
1715
|
+
before := before || jsonb_build_object('timezone', OLD.timezone);
|
|
1716
|
+
after := after || jsonb_build_object('timezone', NEW.timezone);
|
|
1717
|
+
END IF;
|
|
1718
|
+
IF OLD.pool_ref IS DISTINCT FROM NEW.pool_ref THEN
|
|
1719
|
+
before := before || jsonb_build_object('pool', OLD.pool_ref);
|
|
1720
|
+
after := after || jsonb_build_object('pool', NEW.pool_ref);
|
|
1721
|
+
END IF;
|
|
1722
|
+
IF OLD.availability_rules IS DISTINCT FROM NEW.availability_rules THEN
|
|
1723
|
+
before := before || jsonb_build_object('availability', OLD.availability_rules);
|
|
1724
|
+
after := after || jsonb_build_object('availability', NEW.availability_rules);
|
|
1725
|
+
END IF;
|
|
1726
|
+
IF OLD.booking_rules IS DISTINCT FROM NEW.booking_rules THEN
|
|
1727
|
+
before := before || jsonb_build_object('booking_rules', OLD.booking_rules);
|
|
1728
|
+
after := after || jsonb_build_object('booking_rules', NEW.booking_rules);
|
|
1729
|
+
END IF;
|
|
1730
|
+
IF after = '{}'::jsonb THEN RETURN NULL; END IF;
|
|
1731
|
+
kind := 'resource.updated';
|
|
1732
|
+
details := jsonb_build_object('before', before, 'after', after);
|
|
1733
|
+
END IF;
|
|
1734
|
+
INSERT INTO slotlock.outbox (tenant_ref, resource_id, type, subject, data, actor)
|
|
1735
|
+
VALUES (
|
|
1736
|
+
changed.tenant_ref, changed.id, kind, changed.id::text,
|
|
1737
|
+
jsonb_build_object('id', changed.id, 'reference', changed.external_ref) || details,
|
|
1738
|
+
NULLIF(current_setting('slotlock.actor', true), '')
|
|
1739
|
+
);
|
|
1740
|
+
RETURN NULL;
|
|
1741
|
+
END
|
|
1742
|
+
$slotlock_outbox_resource$;
|
|
1743
|
+
|
|
1744
|
+
-- A source first recorded, and deleted. Reading it again (a new revision) is progress, not a change
|
|
1745
|
+
-- of what the resource is read from, and appends nothing; neither does a resource's deletion, which
|
|
1746
|
+
-- takes its coverage with it and is its own change. The source is named by its digest (source_id):
|
|
1747
|
+
-- it can be an iCalendar URL with a token in it, or an address.
|
|
1748
|
+
CREATE OR REPLACE FUNCTION slotlock.outbox_coverage_change()
|
|
1749
|
+
RETURNS trigger
|
|
1750
|
+
LANGUAGE plpgsql
|
|
1751
|
+
SET search_path = pg_catalog, slotlock, pg_temp
|
|
1752
|
+
AS $slotlock_outbox_coverage$
|
|
1753
|
+
DECLARE
|
|
1754
|
+
changed slotlock.calendar_coverage%ROWTYPE;
|
|
1755
|
+
BEGIN
|
|
1756
|
+
changed := CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
|
|
1757
|
+
IF slotlock.outbox_erasing(changed.tenant_ref) THEN RETURN NULL; END IF;
|
|
1758
|
+
IF TG_OP = 'DELETE'
|
|
1759
|
+
AND NOT EXISTS (SELECT 1 FROM slotlock.resources resource WHERE resource.id = OLD.resource_id) THEN
|
|
1760
|
+
RETURN NULL;
|
|
1761
|
+
END IF;
|
|
1762
|
+
INSERT INTO slotlock.outbox (tenant_ref, resource_id, type, subject, data, actor)
|
|
1763
|
+
VALUES (
|
|
1764
|
+
changed.tenant_ref, changed.resource_id,
|
|
1765
|
+
CASE WHEN TG_OP = 'DELETE' THEN 'coverage.deleted' ELSE 'coverage.recorded' END,
|
|
1766
|
+
${slotlockCoverageSourceIdSql('changed.tenant_ref', 'changed.source')},
|
|
1767
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
1768
|
+
'resource_id', changed.resource_id,
|
|
1769
|
+
'source_id', ${slotlockCoverageSourceIdSql('changed.tenant_ref', 'changed.source')},
|
|
1770
|
+
'start', CASE WHEN TG_OP = 'INSERT' THEN changed.starts_at END,
|
|
1771
|
+
'end', CASE WHEN TG_OP = 'INSERT' THEN changed.ends_at END,
|
|
1772
|
+
'revision', CASE WHEN TG_OP = 'INSERT' THEN changed.revision END)),
|
|
1773
|
+
NULLIF(current_setting('slotlock.actor', true), '')
|
|
1774
|
+
);
|
|
1775
|
+
RETURN NULL;
|
|
1776
|
+
END
|
|
1777
|
+
$slotlock_outbox_coverage$;
|
|
1778
|
+
|
|
1779
|
+
-- An endpoint added, enabled again or deleted (disabling is webhook.disabled, from the delivery that
|
|
1780
|
+
-- gave up). Its id and the types it takes, never its URL, which can carry a token, or its secret.
|
|
1781
|
+
CREATE OR REPLACE FUNCTION slotlock.outbox_webhook_change()
|
|
1782
|
+
RETURNS trigger
|
|
1783
|
+
LANGUAGE plpgsql
|
|
1784
|
+
SET search_path = pg_catalog, slotlock, pg_temp
|
|
1785
|
+
AS $slotlock_outbox_webhook$
|
|
1786
|
+
DECLARE
|
|
1787
|
+
changed slotlock.webhook_endpoints%ROWTYPE;
|
|
1788
|
+
kind text;
|
|
1789
|
+
BEGIN
|
|
1790
|
+
changed := CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
|
|
1791
|
+
IF slotlock.outbox_erasing(changed.tenant_ref) THEN RETURN NULL; END IF;
|
|
1792
|
+
IF TG_OP = 'INSERT' THEN
|
|
1793
|
+
kind := 'webhook.created';
|
|
1794
|
+
ELSIF TG_OP = 'DELETE' THEN
|
|
1795
|
+
kind := 'webhook.deleted';
|
|
1796
|
+
ELSIF OLD.disabled_at IS NOT NULL AND NEW.disabled_at IS NULL THEN
|
|
1797
|
+
kind := 'webhook.enabled';
|
|
1798
|
+
ELSE
|
|
1799
|
+
RETURN NULL;
|
|
1800
|
+
END IF;
|
|
1801
|
+
INSERT INTO slotlock.outbox (tenant_ref, type, subject, data, actor)
|
|
1802
|
+
VALUES (
|
|
1803
|
+
changed.tenant_ref, kind, changed.id::text,
|
|
1804
|
+
jsonb_build_object('id', changed.id, 'event_types', to_jsonb(changed.event_types)),
|
|
1805
|
+
NULLIF(current_setting('slotlock.actor', true), '')
|
|
1806
|
+
);
|
|
1807
|
+
RETURN NULL;
|
|
1808
|
+
END
|
|
1809
|
+
$slotlock_outbox_webhook$;
|
|
1810
|
+
|
|
1811
|
+
CREATE OR REPLACE TRIGGER slotlock_outbox_resource_change
|
|
1812
|
+
AFTER INSERT OR UPDATE OR DELETE ON slotlock.resources
|
|
1813
|
+
FOR EACH ROW EXECUTE FUNCTION slotlock.outbox_resource_change();
|
|
1814
|
+
CREATE OR REPLACE TRIGGER slotlock_outbox_coverage_change
|
|
1815
|
+
AFTER INSERT OR DELETE ON slotlock.calendar_coverage
|
|
1816
|
+
FOR EACH ROW EXECUTE FUNCTION slotlock.outbox_coverage_change();
|
|
1817
|
+
CREATE OR REPLACE TRIGGER slotlock_outbox_webhook_change
|
|
1818
|
+
AFTER INSERT OR DELETE ON slotlock.webhook_endpoints
|
|
1819
|
+
FOR EACH ROW EXECUTE FUNCTION slotlock.outbox_webhook_change();
|
|
1820
|
+
-- Only an update that sets disabled_at: leases and deliveries, which do not, cost nothing here.
|
|
1821
|
+
CREATE OR REPLACE TRIGGER slotlock_outbox_webhook_enabled
|
|
1822
|
+
AFTER UPDATE OF disabled_at ON slotlock.webhook_endpoints
|
|
1823
|
+
FOR EACH ROW EXECUTE FUNCTION slotlock.outbox_webhook_change();
|
|
1824
|
+
|
|
1546
1825
|
DROP TRIGGER IF EXISTS slotlock_outbox_reservation_change ON slotlock.reservations;
|
|
1547
1826
|
CREATE TRIGGER slotlock_outbox_reservation_change
|
|
1548
1827
|
AFTER INSERT OR UPDATE OR DELETE ON slotlock.reservations
|
|
@@ -1584,6 +1863,8 @@ CREATE TABLE IF NOT EXISTS slotlock.reservations (
|
|
|
1584
1863
|
ends_at timestamptz NOT NULL,
|
|
1585
1864
|
buffer_after_ms bigint NOT NULL DEFAULT 0,
|
|
1586
1865
|
occupied_ends_at timestamptz,
|
|
1866
|
+
buffer_before_ms bigint NOT NULL DEFAULT 0,
|
|
1867
|
+
occupied_starts_at timestamptz,
|
|
1587
1868
|
revision bigint NOT NULL DEFAULT 1,
|
|
1588
1869
|
status text NOT NULL DEFAULT 'confirmed',
|
|
1589
1870
|
expires_at timestamptz,
|
|
@@ -1604,6 +1885,15 @@ CREATE TABLE IF NOT EXISTS slotlock.reservations (
|
|
|
1604
1885
|
AND occupied_ends_at = ends_at + buffer_after_ms * interval '1 millisecond'
|
|
1605
1886
|
)
|
|
1606
1887
|
),
|
|
1888
|
+
CONSTRAINT slotlock_reservations_buffer_before_valid CHECK (
|
|
1889
|
+
buffer_before_ms >= 0 AND buffer_before_ms <= 86400000
|
|
1890
|
+
),
|
|
1891
|
+
-- NULL only without a leading buffer: a NULL occupied start reads as the start itself.
|
|
1892
|
+
CONSTRAINT slotlock_reservations_occupied_start_valid CHECK (
|
|
1893
|
+
(occupied_starts_at IS NULL AND buffer_before_ms = 0)
|
|
1894
|
+
OR (occupied_starts_at IS NOT NULL
|
|
1895
|
+
AND occupied_starts_at = starts_at - buffer_before_ms * interval '1 millisecond')
|
|
1896
|
+
),
|
|
1607
1897
|
CONSTRAINT slotlock_reservations_status_valid CHECK (status IN ('held', 'confirmed')),
|
|
1608
1898
|
-- A hold must carry its expiry; a confirmed reservation must not (expiry is hold-only state).
|
|
1609
1899
|
CONSTRAINT slotlock_reservations_expiry_shape CHECK (
|
|
@@ -1612,7 +1902,7 @@ CREATE TABLE IF NOT EXISTS slotlock.reservations (
|
|
|
1612
1902
|
),
|
|
1613
1903
|
CONSTRAINT slotlock_reservations_no_overlap EXCLUDE USING gist (
|
|
1614
1904
|
resource_id WITH =,
|
|
1615
|
-
tstzrange(starts_at, COALESCE(occupied_ends_at, ends_at), '[)') WITH &&
|
|
1905
|
+
tstzrange(COALESCE(occupied_starts_at, starts_at), COALESCE(occupied_ends_at, ends_at), '[)') WITH &&
|
|
1616
1906
|
)
|
|
1617
1907
|
);
|
|
1618
1908
|
|
|
@@ -1644,6 +1934,11 @@ ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS tenant_ref text;
|
|
|
1644
1934
|
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS external_ref text;
|
|
1645
1935
|
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS buffer_after_ms bigint NOT NULL DEFAULT 0;
|
|
1646
1936
|
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS occupied_ends_at timestamptz;
|
|
1937
|
+
-- The leading buffer a booking keeps before its start (its resource's bufferBeforeMinutes when it
|
|
1938
|
+
-- was made), and where its occupied time starts: its start less that buffer. A row written before
|
|
1939
|
+
-- leading buffers has none, so its occupied time starts at its start (NULL reads as starts_at).
|
|
1940
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS buffer_before_ms bigint NOT NULL DEFAULT 0;
|
|
1941
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS occupied_starts_at timestamptz;
|
|
1647
1942
|
-- A keyed hold's holder (its principal) and the group of alternatives it was taken in: an agent
|
|
1648
1943
|
-- releases or books only its own holds, and booking one releases the rest of its group.
|
|
1649
1944
|
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS holder_ref text;
|
|
@@ -1681,6 +1976,31 @@ BEGIN
|
|
|
1681
1976
|
END IF;
|
|
1682
1977
|
END $$;
|
|
1683
1978
|
|
|
1979
|
+
-- The occupied start, materialized for the same reason as the occupied end.
|
|
1980
|
+
CREATE OR REPLACE FUNCTION slotlock.set_reservation_occupied_start()
|
|
1981
|
+
RETURNS trigger
|
|
1982
|
+
LANGUAGE plpgsql
|
|
1983
|
+
SET search_path = pg_catalog, slotlock, pg_temp
|
|
1984
|
+
AS $$
|
|
1985
|
+
BEGIN
|
|
1986
|
+
NEW.occupied_starts_at := NEW.starts_at - NEW.buffer_before_ms * interval '1 millisecond';
|
|
1987
|
+
RETURN NEW;
|
|
1988
|
+
END $$;
|
|
1989
|
+
|
|
1990
|
+
DO $$
|
|
1991
|
+
BEGIN
|
|
1992
|
+
IF NOT EXISTS (
|
|
1993
|
+
SELECT 1 FROM pg_trigger
|
|
1994
|
+
WHERE tgname = 'slotlock_reservations_set_occupied_start'
|
|
1995
|
+
AND tgrelid = 'slotlock.reservations'::regclass
|
|
1996
|
+
AND NOT tgisinternal
|
|
1997
|
+
) THEN
|
|
1998
|
+
CREATE TRIGGER slotlock_reservations_set_occupied_start
|
|
1999
|
+
BEFORE INSERT OR UPDATE OF starts_at, buffer_before_ms ON slotlock.reservations
|
|
2000
|
+
FOR EACH ROW EXECUTE FUNCTION slotlock.set_reservation_occupied_start();
|
|
2001
|
+
END IF;
|
|
2002
|
+
END $$;
|
|
2003
|
+
|
|
1684
2004
|
-- Cancellation is an idempotency fact, not the absence of a reservation. Keeping a tombstone
|
|
1685
2005
|
-- prevents delayed at-least-once create delivery from resurrecting a cancelled booking key.
|
|
1686
2006
|
CREATE TABLE IF NOT EXISTS slotlock.reservation_tombstones (
|
|
@@ -1975,6 +2295,34 @@ BEGIN
|
|
|
1975
2295
|
buffer_after_ms >= 0 AND buffer_after_ms <= 2592000000
|
|
1976
2296
|
) NOT VALID;
|
|
1977
2297
|
END IF;
|
|
2298
|
+
-- Every existing row has no leading buffer, so both hold of it; added NOT VALID (no scan under
|
|
2299
|
+
-- the lock) and validated after the migration commits.
|
|
2300
|
+
IF NOT EXISTS (
|
|
2301
|
+
SELECT 1 FROM pg_constraint
|
|
2302
|
+
WHERE conname = 'slotlock_reservations_buffer_before_valid'
|
|
2303
|
+
AND conrelid = 'slotlock.reservations'::regclass
|
|
2304
|
+
) THEN
|
|
2305
|
+
ALTER TABLE slotlock.reservations
|
|
2306
|
+
ADD CONSTRAINT slotlock_reservations_buffer_before_valid CHECK (
|
|
2307
|
+
buffer_before_ms >= 0 AND buffer_before_ms <= 86400000
|
|
2308
|
+
) NOT VALID;
|
|
2309
|
+
END IF;
|
|
2310
|
+
-- A first form of this check let a NULL occupied start through with a leading buffer (the
|
|
2311
|
+
-- comparison is NULL, which a CHECK passes): replaced wherever it lacks the IS NOT NULL guard.
|
|
2312
|
+
IF NOT EXISTS (
|
|
2313
|
+
SELECT 1 FROM pg_constraint
|
|
2314
|
+
WHERE conname = 'slotlock_reservations_occupied_start_valid'
|
|
2315
|
+
AND conrelid = 'slotlock.reservations'::regclass
|
|
2316
|
+
AND pg_catalog.pg_get_constraintdef(oid) LIKE '%occupied_starts_at IS NOT NULL%'
|
|
2317
|
+
) THEN
|
|
2318
|
+
ALTER TABLE slotlock.reservations DROP CONSTRAINT IF EXISTS slotlock_reservations_occupied_start_valid;
|
|
2319
|
+
ALTER TABLE slotlock.reservations
|
|
2320
|
+
ADD CONSTRAINT slotlock_reservations_occupied_start_valid CHECK (
|
|
2321
|
+
(occupied_starts_at IS NULL AND buffer_before_ms = 0)
|
|
2322
|
+
OR (occupied_starts_at IS NOT NULL
|
|
2323
|
+
AND occupied_starts_at = starts_at - buffer_before_ms * interval '1 millisecond')
|
|
2324
|
+
) NOT VALID;
|
|
2325
|
+
END IF;
|
|
1978
2326
|
END $$;
|
|
1979
2327
|
|
|
1980
2328
|
-- Every row occupies from its start to its occupied end: its end plus its turnaround buffer. Up to
|
|
@@ -2025,13 +2373,35 @@ BEGIN
|
|
|
2025
2373
|
ALTER TABLE slotlock.reservations
|
|
2026
2374
|
ADD CONSTRAINT slotlock_reservations_no_overlap EXCLUDE USING gist (
|
|
2027
2375
|
resource_id WITH =,
|
|
2028
|
-
tstzrange(starts_at, COALESCE(occupied_ends_at, ends_at), '[)') WITH &&
|
|
2376
|
+
tstzrange(COALESCE(occupied_starts_at, starts_at), COALESCE(occupied_ends_at, ends_at), '[)') WITH &&
|
|
2029
2377
|
);
|
|
2030
2378
|
END IF;
|
|
2031
2379
|
ALTER TABLE slotlock.reservations DROP CONSTRAINT IF EXISTS slotlock_reservations_buffered_no_overlap;
|
|
2032
2380
|
END
|
|
2033
2381
|
$slotlock_occupied_overlap$;
|
|
2034
2382
|
|
|
2383
|
+
-- A row occupies from its occupied start too: its start less its leading buffer. Every row an
|
|
2384
|
+
-- upgrade finds was written before leading buffers and keeps none, so its range is the one the old
|
|
2385
|
+
-- constraint already kept apart and none can conflict: the constraint is replaced in place, its
|
|
2386
|
+
-- index built once under the migration's lock.
|
|
2387
|
+
DO $slotlock_occupied_start_overlap$
|
|
2388
|
+
BEGIN
|
|
2389
|
+
IF NOT EXISTS (
|
|
2390
|
+
SELECT 1 FROM pg_constraint
|
|
2391
|
+
WHERE conname = 'slotlock_reservations_no_overlap'
|
|
2392
|
+
AND conrelid = 'slotlock.reservations'::regclass
|
|
2393
|
+
AND pg_catalog.pg_get_constraintdef(oid) LIKE '%occupied_starts_at%'
|
|
2394
|
+
) THEN
|
|
2395
|
+
ALTER TABLE slotlock.reservations DROP CONSTRAINT IF EXISTS slotlock_reservations_no_overlap;
|
|
2396
|
+
ALTER TABLE slotlock.reservations
|
|
2397
|
+
ADD CONSTRAINT slotlock_reservations_no_overlap EXCLUDE USING gist (
|
|
2398
|
+
resource_id WITH =,
|
|
2399
|
+
tstzrange(COALESCE(occupied_starts_at, starts_at), COALESCE(occupied_ends_at, ends_at), '[)') WITH &&
|
|
2400
|
+
);
|
|
2401
|
+
END IF;
|
|
2402
|
+
END
|
|
2403
|
+
$slotlock_occupied_start_overlap$;
|
|
2404
|
+
|
|
2035
2405
|
CREATE INDEX IF NOT EXISTS slotlock_reservations_resource_time_idx
|
|
2036
2406
|
ON slotlock.reservations (resource_id, starts_at, ends_at);
|
|
2037
2407
|
|
|
@@ -2231,6 +2601,10 @@ ALTER TABLE slotlock.calendar_events ADD COLUMN IF NOT EXISTS recurrence_ends_by
|
|
|
2231
2601
|
ALTER TABLE slotlock.calendar_events
|
|
2232
2602
|
ADD COLUMN IF NOT EXISTS buffer_after_ms integer NOT NULL DEFAULT 0
|
|
2233
2603
|
CONSTRAINT slotlock_calendar_events_buffer_valid CHECK (buffer_after_ms BETWEEN 0 AND 86400000);
|
|
2604
|
+
-- The leading buffer a booked event keeps before each occurrence, kept for a later roll alike.
|
|
2605
|
+
ALTER TABLE slotlock.calendar_events
|
|
2606
|
+
ADD COLUMN IF NOT EXISTS buffer_before_ms integer NOT NULL DEFAULT 0
|
|
2607
|
+
CONSTRAINT slotlock_calendar_events_buffer_before_valid CHECK (buffer_before_ms BETWEEN 0 AND 86400000);
|
|
2234
2608
|
ALTER TABLE slotlock.calendar_events
|
|
2235
2609
|
DROP CONSTRAINT IF EXISTS slotlock_calendar_events_recurrence_cursor_bounded;
|
|
2236
2610
|
DO $slotlock_calendar_event_recurrence_cursor$
|
|
@@ -2796,6 +3170,7 @@ export const SLOTLOCK_TENANT_TABLES = Object.freeze([
|
|
|
2796
3170
|
'outbox_pruned',
|
|
2797
3171
|
'webhook_endpoints',
|
|
2798
3172
|
'waitlist_entries',
|
|
3173
|
+
'booking_rule_defaults',
|
|
2799
3174
|
'assistant_settings',
|
|
2800
3175
|
'assistant_conversations',
|
|
2801
3176
|
]);
|
|
@@ -2867,6 +3242,8 @@ ALTER TABLE slotlock.webhook_endpoints ENABLE ROW LEVEL SECURITY;
|
|
|
2867
3242
|
ALTER TABLE slotlock.webhook_endpoints FORCE ROW LEVEL SECURITY;
|
|
2868
3243
|
ALTER TABLE slotlock.waitlist_entries ENABLE ROW LEVEL SECURITY;
|
|
2869
3244
|
ALTER TABLE slotlock.waitlist_entries FORCE ROW LEVEL SECURITY;
|
|
3245
|
+
ALTER TABLE slotlock.booking_rule_defaults ENABLE ROW LEVEL SECURITY;
|
|
3246
|
+
ALTER TABLE slotlock.booking_rule_defaults FORCE ROW LEVEL SECURITY;
|
|
2870
3247
|
ALTER TABLE slotlock.assistant_settings ENABLE ROW LEVEL SECURITY;
|
|
2871
3248
|
ALTER TABLE slotlock.assistant_settings FORCE ROW LEVEL SECURITY;
|
|
2872
3249
|
ALTER TABLE slotlock.assistant_conversations ENABLE ROW LEVEL SECURITY;
|
|
@@ -2900,6 +3277,7 @@ BEGIN
|
|
|
2900
3277
|
'outbox_pruned',
|
|
2901
3278
|
'webhook_endpoints',
|
|
2902
3279
|
'waitlist_entries',
|
|
3280
|
+
'booking_rule_defaults',
|
|
2903
3281
|
'assistant_settings',
|
|
2904
3282
|
'assistant_conversations'
|
|
2905
3283
|
)
|
|
@@ -3113,6 +3491,16 @@ BEGIN
|
|
|
3113
3491
|
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
3114
3492
|
END IF;
|
|
3115
3493
|
|
|
3494
|
+
IF NOT EXISTS (
|
|
3495
|
+
SELECT 1 FROM pg_policies
|
|
3496
|
+
WHERE schemaname = 'slotlock' AND tablename = 'booking_rule_defaults'
|
|
3497
|
+
AND policyname = 'tenant_isolation_v1'
|
|
3498
|
+
) THEN
|
|
3499
|
+
CREATE POLICY tenant_isolation_v1 ON slotlock.booking_rule_defaults
|
|
3500
|
+
USING (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''))
|
|
3501
|
+
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
3502
|
+
END IF;
|
|
3503
|
+
|
|
3116
3504
|
IF NOT EXISTS (
|
|
3117
3505
|
SELECT 1 FROM pg_policies
|
|
3118
3506
|
WHERE schemaname = 'slotlock' AND tablename = 'assistant_settings'
|
|
@@ -3169,6 +3557,8 @@ COMMENT ON POLICY tenant_isolation_v1 ON slotlock.webhook_endpoints
|
|
|
3169
3557
|
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
3170
3558
|
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.waitlist_entries
|
|
3171
3559
|
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
3560
|
+
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.booking_rule_defaults
|
|
3561
|
+
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
3172
3562
|
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.assistant_settings
|
|
3173
3563
|
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
3174
3564
|
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.assistant_conversations
|
|
@@ -3213,6 +3603,7 @@ WHERE schemaname = 'slotlock'
|
|
|
3213
3603
|
'outbox_pruned',
|
|
3214
3604
|
'webhook_endpoints',
|
|
3215
3605
|
'waitlist_entries',
|
|
3606
|
+
'booking_rule_defaults',
|
|
3216
3607
|
'assistant_settings',
|
|
3217
3608
|
'assistant_conversations'
|
|
3218
3609
|
)
|
package/event-feed.d.ts
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
import type { Sql, TransactionSql } from 'postgres';
|
|
2
2
|
type SlotlockSql = Sql | TransactionSql;
|
|
3
3
|
/** What a change can be, as the feed and a webhook's `event_types` name it. */
|
|
4
|
-
export declare const SLOTLOCK_CHANGE_TYPES: readonly ["hold.created", "hold.updated", "hold.confirmed", "hold.released", "hold.expired", "reservation.created", "reservation.updated", "reservation.cancelled", "reservation.checked_in", "reservation.no_show", "event.created", "event.updated", "event.cancelled", "event.timezone_shifted", "event.timezone_conflict", "coverage.stale", "waitlist.offered", "waitlist.expired", "webhook.disabled"];
|
|
4
|
+
export declare const SLOTLOCK_CHANGE_TYPES: readonly ["hold.created", "hold.updated", "hold.confirmed", "hold.released", "hold.expired", "reservation.created", "reservation.updated", "reservation.cancelled", "reservation.checked_in", "reservation.no_show", "event.created", "event.updated", "event.cancelled", "event.timezone_shifted", "event.timezone_conflict", "coverage.stale", "waitlist.offered", "waitlist.expired", "webhook.disabled", "resource.created", "resource.updated", "resource.archived", "resource.restored", "resource.deleted", "coverage.recorded", "coverage.deleted", "webhook.created", "webhook.enabled", "webhook.deleted", "key.created", "key.rotated", "key.revoked"];
|
|
5
5
|
export type SlotlockChangeType = (typeof SLOTLOCK_CHANGE_TYPES)[number];
|
|
6
|
+
/**
|
|
7
|
+
* Where a change came from, as the history and the feed record it (withTenant's `origin`): an agent
|
|
8
|
+
* protocol (`mcp`, `a2a`), the REST API's agent routes (`rest`) or its application operations
|
|
9
|
+
* (`application`), the dashboard, a member's dashboard assistant, maintenance, or code calling the
|
|
10
|
+
* store itself (`library`). Never an address or anything else about a person.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SLOTLOCK_CHANGE_ORIGINS: readonly ["mcp", "a2a", "rest", "application", "dashboard", "dashboard-assistant", "maintenance", "library"];
|
|
13
|
+
export type SlotlockChangeOrigin = (typeof SLOTLOCK_CHANGE_ORIGINS)[number];
|
|
14
|
+
/** Whether `value` is one of SLOTLOCK_CHANGE_ORIGINS. */
|
|
15
|
+
export declare function isSlotlockChangeOrigin(value: unknown): value is SlotlockChangeOrigin;
|
|
16
|
+
/**
|
|
17
|
+
* The SQL that names a coverage source on the feed (`coverage.recorded`, `.deleted`, `.stale`): a
|
|
18
|
+
* digest of the tenant and the source, stable for both, so the source itself (an iCalendar URL with
|
|
19
|
+
* a token, an address) never reaches a webhook or the history. `tenant` and `source` are SQL
|
|
20
|
+
* expressions; slotlockCoverageSourceId works out the same value.
|
|
21
|
+
*/
|
|
22
|
+
export declare function slotlockCoverageSourceIdSql(tenant: string, source: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* How the feed names a tenant's coverage source (`subject` and `data.source_id` of `coverage.*`
|
|
25
|
+
* changes; `source_id` of slotlock_get_coverage): `src_` and 24 hex digits.
|
|
26
|
+
*/
|
|
27
|
+
export declare function slotlockCoverageSourceId(tenantRef: string, source: string): string;
|
|
6
28
|
/** How long the feed keeps a change, and so how far back a cursor may reach. */
|
|
7
29
|
export declare const SLOTLOCK_CHANGE_RETENTION_DAYS = 30;
|
|
8
30
|
/** Webhook endpoints one tenant may have. */
|
|
@@ -20,6 +42,10 @@ export interface SlotlockChange {
|
|
|
20
42
|
occurredAt: Date;
|
|
21
43
|
/** Who made it (withTenant's `actor`: an API key, an agent), or null when unrecorded. */
|
|
22
44
|
actor: string | null;
|
|
45
|
+
/** Where it came from (withTenant's `origin`), or null when unrecorded (before 0.6, say). */
|
|
46
|
+
origin: SlotlockChangeOrigin | null;
|
|
47
|
+
/** The API key that made it (withTenant's `keyId`), or null when no key did. */
|
|
48
|
+
keyId: string | null;
|
|
23
49
|
/** Pass as `after` to read what follows this change. */
|
|
24
50
|
position: string;
|
|
25
51
|
}
|
|
@@ -150,9 +176,10 @@ export interface SlotlockEventFeed {
|
|
|
150
176
|
};
|
|
151
177
|
}): Promise<void>;
|
|
152
178
|
/**
|
|
153
|
-
* History: the changes to one resource (`resourceId
|
|
154
|
-
* reference, an event's
|
|
155
|
-
*
|
|
179
|
+
* History: the changes to one resource (`resourceId`: its bookings and events, and its own
|
|
180
|
+
* configuration) or one record (`subject`: a booking's reference, an event's, hold's, webhook's or
|
|
181
|
+
* API key's id), newest first, `limit` (1-100) at a time, each with who made it (`actor`, `keyId`),
|
|
182
|
+
* where from (`origin`) and, for a change of an existing record, what it was before. Pass the last one's
|
|
156
183
|
* `id` as `before` for the page after. Kept as long as the feed keeps changes.
|
|
157
184
|
*/
|
|
158
185
|
listHistory(params: {
|
|
@@ -175,4 +202,10 @@ export declare function isWebhookUrl(value: unknown): value is string;
|
|
|
175
202
|
* `retentionDays` (default SLOTLOCK_CHANGE_RETENTION_DAYS), and a cursor reaches back as far.
|
|
176
203
|
*/
|
|
177
204
|
export declare function slotlockEventFeedMethods(sql: SlotlockSql, retentionDays?: number): SlotlockEventFeed;
|
|
205
|
+
/**
|
|
206
|
+
* A position at or before every change that has not ended yet, so a reader that starts from it and
|
|
207
|
+
* follows `next` misses none still to come (it may read again some that ended just before). Live
|
|
208
|
+
* updates start here (agent-store-backend.ts, createSlotlockStoreChangeSource).
|
|
209
|
+
*/
|
|
210
|
+
export declare function slotlockChangeFeedHead(sql: SlotlockSql): Promise<string>;
|
|
178
211
|
export {};
|