slotlock 0.8.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 +237 -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 +34 -8
- package/assistant.js +81 -26
- 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 +1 -1
- package/dashboard-assistant-ui.js +109 -8
- package/dashboard.d.ts +55 -6
- package/dashboard.js +790 -105
- 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/ddl.js
CHANGED
|
@@ -292,6 +292,10 @@ export const SLOTLOCK_QUOTA_FUNCTIONS = Object.freeze([
|
|
|
292
292
|
]);
|
|
293
293
|
/** What a plan's monthly quotas count (quotas.ts): operations run and bookings made. */
|
|
294
294
|
export const SLOTLOCK_QUOTA_METRICS = Object.freeze(['operations', 'bookings']);
|
|
295
|
+
/** The most conversations a member keeps with the dashboard assistant: past it, the least recent goes. */
|
|
296
|
+
export const SLOTLOCK_ASSISTANT_MAX_CHATS = 100;
|
|
297
|
+
/** How a conversation with the assistant is named: 16 URL-safe characters. */
|
|
298
|
+
export const SLOTLOCK_ASSISTANT_CHAT_ID = /^[A-Za-z0-9_-]{16}$/;
|
|
295
299
|
/** The shares of a quota, in percent, at which a tenant is told once a month (usage.threshold_reached). */
|
|
296
300
|
export const SLOTLOCK_QUOTA_NOTICE_PERCENTS = Object.freeze([80, 100]);
|
|
297
301
|
/** The most bookings past the roll-up cursor one quota read counts: more than any plan allows. */
|
|
@@ -1948,6 +1952,38 @@ BEGIN
|
|
|
1948
1952
|
DELETE FROM slotlock.approval_policies WHERE tenant_ref = requested_tenant_ref;
|
|
1949
1953
|
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
1950
1954
|
RETURN NEXT;
|
|
1955
|
+
erased_table := 'availability_exceptions';
|
|
1956
|
+
DELETE FROM slotlock.availability_exceptions WHERE tenant_ref = requested_tenant_ref;
|
|
1957
|
+
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
1958
|
+
RETURN NEXT;
|
|
1959
|
+
erased_table := 'holiday_calendars';
|
|
1960
|
+
DELETE FROM slotlock.holiday_calendars WHERE tenant_ref = requested_tenant_ref;
|
|
1961
|
+
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
1962
|
+
RETURN NEXT;
|
|
1963
|
+
erased_table := 'availability_defaults';
|
|
1964
|
+
DELETE FROM slotlock.availability_defaults WHERE tenant_ref = requested_tenant_ref;
|
|
1965
|
+
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
1966
|
+
RETURN NEXT;
|
|
1967
|
+
erased_table := 'contacts';
|
|
1968
|
+
DELETE FROM slotlock.contacts WHERE tenant_ref = requested_tenant_ref;
|
|
1969
|
+
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
1970
|
+
RETURN NEXT;
|
|
1971
|
+
erased_table := 'reminder_dispatches';
|
|
1972
|
+
DELETE FROM slotlock.reminder_dispatches WHERE tenant_ref = requested_tenant_ref;
|
|
1973
|
+
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
1974
|
+
RETURN NEXT;
|
|
1975
|
+
erased_table := 'reservation_reminder_dispatches';
|
|
1976
|
+
DELETE FROM slotlock.reservation_reminder_dispatches WHERE tenant_ref = requested_tenant_ref;
|
|
1977
|
+
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
1978
|
+
RETURN NEXT;
|
|
1979
|
+
erased_table := 'hold_payments';
|
|
1980
|
+
DELETE FROM slotlock.hold_payments WHERE tenant_ref = requested_tenant_ref;
|
|
1981
|
+
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
1982
|
+
RETURN NEXT;
|
|
1983
|
+
erased_table := 'source_busy';
|
|
1984
|
+
DELETE FROM slotlock.source_busy WHERE tenant_ref = requested_tenant_ref;
|
|
1985
|
+
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
1986
|
+
RETURN NEXT;
|
|
1951
1987
|
erased_table := 'reservations';
|
|
1952
1988
|
DELETE FROM slotlock.reservations
|
|
1953
1989
|
WHERE tenant_ref = requested_tenant_ref
|
|
@@ -2031,6 +2067,10 @@ BEGIN
|
|
|
2031
2067
|
DELETE FROM slotlock.assistant_conversations WHERE tenant_ref = requested_tenant_ref;
|
|
2032
2068
|
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
2033
2069
|
RETURN NEXT;
|
|
2070
|
+
erased_table := 'assistant_chats';
|
|
2071
|
+
DELETE FROM slotlock.assistant_chats WHERE tenant_ref = requested_tenant_ref;
|
|
2072
|
+
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
2073
|
+
RETURN NEXT;
|
|
2034
2074
|
erased_table := 'assistant_settings';
|
|
2035
2075
|
DELETE FROM slotlock.assistant_settings WHERE tenant_ref = requested_tenant_ref;
|
|
2036
2076
|
GET DIAGNOSTICS erased_rows = ROW_COUNT;
|
|
@@ -2199,7 +2239,9 @@ BEGIN
|
|
|
2199
2239
|
FOREACH emptied IN ARRAY ARRAY[
|
|
2200
2240
|
'calendar_event_attendees', 'calendar_event_reminders', 'calendar_event_exceptions',
|
|
2201
2241
|
'calendar_event_occurrences', 'composite_claims', 'approvals', 'approval_policies',
|
|
2202
|
-
'
|
|
2242
|
+
'availability_exceptions', 'holiday_calendars', 'availability_defaults', 'contacts', 'reminder_dispatches',
|
|
2243
|
+
'reservation_reminder_dispatches',
|
|
2244
|
+
'hold_payments', 'source_busy', 'calendar_events', 'calendar_event_tombstones', 'calendar_event_commands',
|
|
2203
2245
|
'reservation_tombstones', 'reservation_conflict_archive', 'calendar_coverage', 'hold_commands',
|
|
2204
2246
|
'waitlist_entries', 'booking_rule_defaults', 'pool_settings', 'resource_group_members',
|
|
2205
2247
|
'resources', 'resource_groups', 'outbox'
|
|
@@ -2407,8 +2449,21 @@ BEGIN
|
|
|
2407
2449
|
AND gone.expires_at <= now();
|
|
2408
2450
|
GET DIAGNOSTICS step = ROW_COUNT;
|
|
2409
2451
|
deleted := deleted + step;
|
|
2410
|
-
|
|
2411
|
-
WHERE
|
|
2452
|
+
DELETE FROM slotlock.assistant_chats gone
|
|
2453
|
+
WHERE gone.tenant_ref = due.tenant_ref
|
|
2454
|
+
AND (gone.member_ref, gone.chat_id) IN (
|
|
2455
|
+
SELECT expired.member_ref, expired.chat_id FROM slotlock.assistant_chats expired
|
|
2456
|
+
WHERE expired.tenant_ref = due.tenant_ref AND expired.expires_at <= now()
|
|
2457
|
+
LIMIT greatest(max_rows - deleted, 0))
|
|
2458
|
+
AND gone.expires_at <= now();
|
|
2459
|
+
GET DIAGNOSTICS step = ROW_COUNT;
|
|
2460
|
+
deleted := deleted + step;
|
|
2461
|
+
SELECT least(
|
|
2462
|
+
(SELECT min(live.expires_at) FROM slotlock.assistant_conversations live
|
|
2463
|
+
WHERE live.tenant_ref = due.tenant_ref),
|
|
2464
|
+
(SELECT min(chat.expires_at) FROM slotlock.assistant_chats chat
|
|
2465
|
+
WHERE chat.tenant_ref = due.tenant_ref AND chat.expires_at IS NOT NULL))
|
|
2466
|
+
INTO upcoming;
|
|
2412
2467
|
IF upcoming IS NULL THEN
|
|
2413
2468
|
DELETE FROM slotlock.assistant_tenants WHERE tenant_ref = due.tenant_ref;
|
|
2414
2469
|
ELSE
|
|
@@ -2750,7 +2805,7 @@ AS $slotlock_location_path$
|
|
|
2750
2805
|
FROM slotlock.locations place JOIN up ON place.tenant_ref = requested_tenant_ref AND place.ref = up.parent_ref
|
|
2751
2806
|
WHERE up.steps <= ${SLOTLOCK_MAX_LOCATION_DEPTH}
|
|
2752
2807
|
)
|
|
2753
|
-
SELECT COALESCE(array_agg(up.ref), '{}'::text[]) FROM up;
|
|
2808
|
+
SELECT COALESCE(array_agg(up.ref ORDER BY up.steps), '{}'::text[]) FROM up;
|
|
2754
2809
|
$slotlock_location_path$;
|
|
2755
2810
|
-- 0.5 allowed a filter of at most 20 types, fewer than there are now. The wider CHECK admits every
|
|
2756
2811
|
-- row the old one did, so it is added NOT VALID (no scan under the lock), and the old one dropped.
|
|
@@ -2821,6 +2876,78 @@ END
|
|
|
2821
2876
|
$slotlock_waitlist_key$;
|
|
2822
2877
|
CREATE UNIQUE INDEX IF NOT EXISTS slotlock_waitlist_entries_key_idx
|
|
2823
2878
|
ON slotlock.waitlist_entries (tenant_ref, idempotency_key) WHERE idempotency_key IS NOT NULL;
|
|
2879
|
+
-- Waitlists that match (RFC 0004 B6): an entry may ask for any slot of a length within its window
|
|
2880
|
+
-- (duration_ms; NULL is the whole window), and an offer ends accepted (its hold confirmed), declined
|
|
2881
|
+
-- (let go) or lapsed (expired), when it was decided. The wider checks admit every row the old ones
|
|
2882
|
+
-- did, so they are added NOT VALID (no scan under the lock), and the old ones dropped.
|
|
2883
|
+
ALTER TABLE slotlock.waitlist_entries ADD COLUMN IF NOT EXISTS duration_ms bigint;
|
|
2884
|
+
ALTER TABLE slotlock.waitlist_entries ADD COLUMN IF NOT EXISTS decided_at timestamptz;
|
|
2885
|
+
DO $slotlock_waitlist_outcomes$
|
|
2886
|
+
BEGIN
|
|
2887
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'slotlock_waitlist_entries_outcome_valid'
|
|
2888
|
+
AND conrelid = 'slotlock.waitlist_entries'::regclass) THEN
|
|
2889
|
+
ALTER TABLE slotlock.waitlist_entries ADD CONSTRAINT slotlock_waitlist_entries_outcome_valid CHECK (
|
|
2890
|
+
status IN ('waiting', 'offered', 'accepted', 'declined', 'lapsed', 'expired')
|
|
2891
|
+
AND (status <> 'offered' OR (hold_id IS NOT NULL AND offered_at IS NOT NULL AND offered_resource_id IS NOT NULL))
|
|
2892
|
+
AND (status <> 'waiting' OR (hold_id IS NULL AND offered_at IS NULL))
|
|
2893
|
+
AND (duration_ms IS NULL OR (duration_ms >= 60000
|
|
2894
|
+
AND duration_ms <= extract(epoch FROM ends_at - starts_at) * 1000))
|
|
2895
|
+
) NOT VALID;
|
|
2896
|
+
END IF;
|
|
2897
|
+
ALTER TABLE slotlock.waitlist_entries DROP CONSTRAINT IF EXISTS slotlock_waitlist_entries_status_valid;
|
|
2898
|
+
ALTER TABLE slotlock.waitlist_entries DROP CONSTRAINT IF EXISTS slotlock_waitlist_entries_offer_valid;
|
|
2899
|
+
END
|
|
2900
|
+
$slotlock_waitlist_outcomes$;
|
|
2901
|
+
|
|
2902
|
+
-- How an offer ends (RFC 0004 B6), from what becomes of its hold, whatever path took it: confirmed
|
|
2903
|
+
-- (or consumed by a booking that names it, under slotlock.ending = 'confirmed') is accepted;
|
|
2904
|
+
-- expired is lapsed; let go before then is declined. The entry says so, and the feed.
|
|
2905
|
+
CREATE OR REPLACE FUNCTION slotlock.waitlist_offer_settle()
|
|
2906
|
+
RETURNS trigger
|
|
2907
|
+
LANGUAGE plpgsql
|
|
2908
|
+
SET search_path = pg_catalog, slotlock, pg_temp
|
|
2909
|
+
AS $slotlock_waitlist_offer_settle$
|
|
2910
|
+
DECLARE
|
|
2911
|
+
outcome text;
|
|
2912
|
+
settled slotlock.waitlist_entries%ROWTYPE;
|
|
2913
|
+
BEGIN
|
|
2914
|
+
IF OLD.status <> 'held' OR OLD.source IS DISTINCT FROM 'slotlock:waitlist' THEN RETURN NULL; END IF;
|
|
2915
|
+
IF TG_OP = 'UPDATE' THEN
|
|
2916
|
+
IF NEW.status = 'held' THEN RETURN NULL; END IF;
|
|
2917
|
+
outcome := 'accepted';
|
|
2918
|
+
ELSIF current_setting('slotlock.ending', true) = 'confirmed' THEN
|
|
2919
|
+
outcome := 'accepted';
|
|
2920
|
+
ELSIF OLD.expires_at <= statement_timestamp() THEN
|
|
2921
|
+
outcome := 'lapsed';
|
|
2922
|
+
ELSE
|
|
2923
|
+
outcome := 'declined';
|
|
2924
|
+
END IF;
|
|
2925
|
+
UPDATE slotlock.waitlist_entries SET status = outcome, decided_at = statement_timestamp()
|
|
2926
|
+
WHERE hold_id = OLD.id AND status = 'offered'
|
|
2927
|
+
RETURNING * INTO settled;
|
|
2928
|
+
IF settled.id IS NULL THEN RETURN NULL; END IF;
|
|
2929
|
+
-- Erasure (erase_tenant, running as the schema's owner) is not reported.
|
|
2930
|
+
IF settled.tenant_ref = current_setting('slotlock.erasing', true)
|
|
2931
|
+
AND current_user = (SELECT pg_catalog.pg_get_userbyid(nspowner) FROM pg_catalog.pg_namespace
|
|
2932
|
+
WHERE nspname = 'slotlock') THEN
|
|
2933
|
+
RETURN NULL;
|
|
2934
|
+
END IF;
|
|
2935
|
+
INSERT INTO slotlock.outbox (tenant_ref, resource_id, type, subject, data, actor)
|
|
2936
|
+
VALUES (
|
|
2937
|
+
settled.tenant_ref, settled.offered_resource_id, 'waitlist.' || outcome, settled.id::text,
|
|
2938
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
2939
|
+
'id', settled.id, 'hold_id', settled.hold_id, 'resource_id', settled.offered_resource_id,
|
|
2940
|
+
'pool', settled.pool_ref, 'holder_ref', settled.holder_ref)),
|
|
2941
|
+
COALESCE(NULLIF(current_setting('slotlock.actor', true), ''),
|
|
2942
|
+
CASE WHEN outcome = 'lapsed' THEN 'slotlock:maintenance' END)
|
|
2943
|
+
);
|
|
2944
|
+
RETURN NULL;
|
|
2945
|
+
END
|
|
2946
|
+
$slotlock_waitlist_offer_settle$;
|
|
2947
|
+
DROP TRIGGER IF EXISTS slotlock_waitlist_offer_settle ON slotlock.reservations;
|
|
2948
|
+
CREATE TRIGGER slotlock_waitlist_offer_settle
|
|
2949
|
+
AFTER UPDATE OF status OR DELETE ON slotlock.reservations
|
|
2950
|
+
FOR EACH ROW EXECUTE FUNCTION slotlock.waitlist_offer_settle();
|
|
2824
2951
|
CREATE INDEX IF NOT EXISTS slotlock_waitlist_entries_tenant_idx
|
|
2825
2952
|
ON slotlock.waitlist_entries (tenant_ref, starts_at);
|
|
2826
2953
|
|
|
@@ -2929,6 +3056,63 @@ CREATE TABLE IF NOT EXISTS slotlock.assistant_conversations (
|
|
|
2929
3056
|
CREATE INDEX IF NOT EXISTS slotlock_assistant_conversations_expiry_idx
|
|
2930
3057
|
ON slotlock.assistant_conversations (tenant_ref, expires_at);
|
|
2931
3058
|
|
|
3059
|
+
-- A member's conversations with the dashboard assistant (its history), each with the writes it
|
|
3060
|
+
-- proposed. Kept until the member deletes one, or until expires_at where the deployment gives
|
|
3061
|
+
-- conversations a lifetime; at most ${SLOTLOCK_ASSISTANT_MAX_CHATS} a member, the least recent going first.
|
|
3062
|
+
CREATE TABLE IF NOT EXISTS slotlock.assistant_chats (
|
|
3063
|
+
tenant_ref text NOT NULL,
|
|
3064
|
+
member_ref text NOT NULL,
|
|
3065
|
+
chat_id text NOT NULL,
|
|
3066
|
+
title text NOT NULL,
|
|
3067
|
+
messages jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
3068
|
+
actions jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
3069
|
+
created_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
|
3070
|
+
updated_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
|
3071
|
+
expires_at timestamptz,
|
|
3072
|
+
CONSTRAINT slotlock_assistant_chats_pkey PRIMARY KEY (tenant_ref, member_ref, chat_id),
|
|
3073
|
+
CONSTRAINT slotlock_assistant_chats_identity_valid CHECK (
|
|
3074
|
+
octet_length(tenant_ref) BETWEEN 1 AND 500 AND octet_length(member_ref) BETWEEN 1 AND 500
|
|
3075
|
+
AND chat_id ~ '^[A-Za-z0-9_-]{16}$'
|
|
3076
|
+
),
|
|
3077
|
+
CONSTRAINT slotlock_assistant_chats_title_valid CHECK (char_length(title) BETWEEN 1 AND 120),
|
|
3078
|
+
CONSTRAINT slotlock_assistant_chats_messages_valid CHECK (
|
|
3079
|
+
jsonb_typeof(messages) = 'array' AND octet_length(messages::text) <= 262144
|
|
3080
|
+
),
|
|
3081
|
+
CONSTRAINT slotlock_assistant_chats_actions_valid CHECK (
|
|
3082
|
+
jsonb_typeof(actions) = 'object' AND octet_length(actions::text) <= 131072
|
|
3083
|
+
)
|
|
3084
|
+
);
|
|
3085
|
+
CREATE INDEX IF NOT EXISTS slotlock_assistant_chats_recent_idx
|
|
3086
|
+
ON slotlock.assistant_chats (tenant_ref, member_ref, updated_at DESC);
|
|
3087
|
+
CREATE INDEX IF NOT EXISTS slotlock_assistant_chats_expiry_idx
|
|
3088
|
+
ON slotlock.assistant_chats (tenant_ref, expires_at) WHERE expires_at IS NOT NULL;
|
|
3089
|
+
|
|
3090
|
+
-- The one conversation a member had before chats were kept (0.8 and earlier) becomes their first
|
|
3091
|
+
-- chat, titled by its first message, and leaves the old table: a later migration does not bring
|
|
3092
|
+
-- back a chat the member deleted. A server of 0.8 still running meanwhile keeps using the old table;
|
|
3093
|
+
-- what it writes there is moved by the next migration as a chat of its own (its id is the
|
|
3094
|
+
-- conversation's, with when it was last written), never into one already moved.
|
|
3095
|
+
WITH moved AS (
|
|
3096
|
+
DELETE FROM slotlock.assistant_conversations old
|
|
3097
|
+
WHERE old.expires_at > statement_timestamp()
|
|
3098
|
+
RETURNING old.tenant_ref, old.member_ref, old.messages, old.actions, old.updated_at
|
|
3099
|
+
)
|
|
3100
|
+
INSERT INTO slotlock.assistant_chats AS chat
|
|
3101
|
+
(tenant_ref, member_ref, chat_id, title, messages, actions, created_at, updated_at, expires_at)
|
|
3102
|
+
SELECT moved.tenant_ref, moved.member_ref,
|
|
3103
|
+
pg_catalog.translate(pg_catalog.left(pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
|
|
3104
|
+
moved.tenant_ref || pg_catalog.chr(10) || moved.member_ref || pg_catalog.chr(10)
|
|
3105
|
+
|| (extract(epoch FROM moved.updated_at) * 1000000)::bigint::text, 'UTF8')), 'base64'), 16), '+/', '-_'),
|
|
3106
|
+
coalesce(nullif(pg_catalog.left(pg_catalog.regexp_replace(pg_catalog.btrim(first_user.text), '\\s+', ' ', 'g'), 80), ''),
|
|
3107
|
+
'Conversation'),
|
|
3108
|
+
moved.messages, moved.actions, moved.updated_at, moved.updated_at, NULL
|
|
3109
|
+
FROM moved
|
|
3110
|
+
LEFT JOIN LATERAL (
|
|
3111
|
+
SELECT entry ->> 'text' AS text FROM pg_catalog.jsonb_array_elements(moved.messages) AS entry
|
|
3112
|
+
WHERE entry ->> 'role' = 'user' LIMIT 1
|
|
3113
|
+
) first_user ON true
|
|
3114
|
+
ON CONFLICT DO NOTHING;
|
|
3115
|
+
|
|
2932
3116
|
-- The tenant a reservation row belongs to: its own, or (a hold) its resource's.
|
|
2933
3117
|
CREATE OR REPLACE FUNCTION slotlock.outbox_reservation_change()
|
|
2934
3118
|
RETURNS trigger
|
|
@@ -2980,8 +3164,8 @@ BEGIN
|
|
|
2980
3164
|
END;
|
|
2981
3165
|
END IF;
|
|
2982
3166
|
INSERT INTO slotlock.outbox (tenant_ref, resource_id, type, subject, data, actor)
|
|
2983
|
-
|
|
2984
|
-
owner, changed.resource_id,
|
|
3167
|
+
SELECT
|
|
3168
|
+
owner, changed.resource_id, written.change_type,
|
|
2985
3169
|
COALESCE(changed.external_ref, changed.id::text),
|
|
2986
3170
|
jsonb_strip_nulls(jsonb_build_object(
|
|
2987
3171
|
'id', changed.id,
|
|
@@ -2990,6 +3174,14 @@ BEGIN
|
|
|
2990
3174
|
'start', changed.starts_at,
|
|
2991
3175
|
'end', changed.ends_at,
|
|
2992
3176
|
'buffer_after_ms', changed.buffer_after_ms,
|
|
3177
|
+
-- Whom to tell and whether (RFC 0004 P3): a reference, never the person; notify only when off.
|
|
3178
|
+
'contact_ref', changed.contact_ref,
|
|
3179
|
+
'notify', CASE WHEN NOT changed.notify THEN false END,
|
|
3180
|
+
'payment_required', CASE WHEN changed.payment_required THEN true END,
|
|
3181
|
+
-- A cancellation's fee and an admin's reason for going past a cutoff (RFC 0004 B5).
|
|
3182
|
+
'cancellation_fee', CASE WHEN kind = 'reservation.cancelled'
|
|
3183
|
+
THEN NULLIF(current_setting('slotlock.cancellation_fee', true), '')::jsonb END,
|
|
3184
|
+
'override_reason', NULLIF(current_setting('slotlock.override_reason', true), ''),
|
|
2993
3185
|
-- A no-show carries the revision its reference is retired at, and the check-in window.
|
|
2994
3186
|
'revision', CASE WHEN changed.status = 'held' THEN NULL
|
|
2995
3187
|
WHEN kind = 'reservation.no_show' THEN changed.revision + 1
|
|
@@ -3003,7 +3195,13 @@ BEGIN
|
|
|
3003
3195
|
'revision', OLD.revision) END
|
|
3004
3196
|
)),
|
|
3005
3197
|
NULLIF(current_setting('slotlock.actor', true), '')
|
|
3006
|
-
|
|
3198
|
+
-- A booking moved in time or to another resource is also reservation.rescheduled (RFC 0004
|
|
3199
|
+
-- B4); reservation.updated stays for those who read it.
|
|
3200
|
+
FROM (VALUES (kind), (CASE WHEN kind = 'reservation.updated'
|
|
3201
|
+
AND (OLD.starts_at, OLD.ends_at, OLD.resource_id)
|
|
3202
|
+
IS DISTINCT FROM (NEW.starts_at, NEW.ends_at, NEW.resource_id)
|
|
3203
|
+
THEN 'reservation.rescheduled' END)) AS written(change_type)
|
|
3204
|
+
WHERE written.change_type IS NOT NULL;
|
|
3007
3205
|
RETURN NULL;
|
|
3008
3206
|
END
|
|
3009
3207
|
$slotlock_outbox_reservation$;
|
|
@@ -3047,6 +3245,11 @@ BEGIN
|
|
|
3047
3245
|
'transparency', changed.transparency,
|
|
3048
3246
|
'recurring', changed.recurrence_rule IS NOT NULL,
|
|
3049
3247
|
'revision', changed.revision,
|
|
3248
|
+
'notify', CASE WHEN NOT changed.notify THEN false END,
|
|
3249
|
+
-- A cancellation's fee and an admin's reason for going past a cutoff (RFC 0004 B5).
|
|
3250
|
+
'cancellation_fee', CASE WHEN kind = 'event.cancelled'
|
|
3251
|
+
THEN NULLIF(current_setting('slotlock.cancellation_fee', true), '')::jsonb END,
|
|
3252
|
+
'override_reason', NULLIF(current_setting('slotlock.override_reason', true), ''),
|
|
3050
3253
|
'before', CASE WHEN TG_OP = 'UPDATE' THEN jsonb_build_object(
|
|
3051
3254
|
'resource_id', OLD.resource_id, 'start', OLD.starts_at, 'end', OLD.ends_at,
|
|
3052
3255
|
'status', OLD.status, 'transparency', OLD.transparency, 'revision', OLD.revision) END
|
|
@@ -3983,6 +4186,12 @@ CREATE INDEX IF NOT EXISTS slotlock_resources_labels_idx ON slotlock.resources U
|
|
|
3983
4186
|
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS tenant_ref text;
|
|
3984
4187
|
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS external_ref text;
|
|
3985
4188
|
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS buffer_after_ms bigint NOT NULL DEFAULT 0;
|
|
4189
|
+
-- When maintenance said the booking ended (reservation.completed, RFC 0004 B4): it changes nothing
|
|
4190
|
+
-- anyone wrote, so the feed's trigger ignores it.
|
|
4191
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS completed_at timestamptz;
|
|
4192
|
+
CREATE INDEX IF NOT EXISTS slotlock_reservations_uncompleted_idx
|
|
4193
|
+
ON slotlock.reservations (tenant_ref, ends_at)
|
|
4194
|
+
WHERE status = 'confirmed' AND completed_at IS NULL;
|
|
3986
4195
|
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS occupied_ends_at timestamptz;
|
|
3987
4196
|
-- The leading buffer a booking keeps before its start (its resource's bufferBeforeMinutes when it
|
|
3988
4197
|
-- was made), and where its occupied time starts: its start less that buffer. A row written before
|
|
@@ -4078,6 +4287,31 @@ ALTER TABLE slotlock.reservation_tombstones ALTER COLUMN cancelled_at SET DEFAUL
|
|
|
4078
4287
|
-- are kept too.
|
|
4079
4288
|
ALTER TABLE slotlock.reservation_tombstones
|
|
4080
4289
|
ADD COLUMN IF NOT EXISTS stray boolean NOT NULL DEFAULT false;
|
|
4290
|
+
-- Why the booking ended (RFC 0004 B4): cancelled or no_show (a hold leaves no tombstone; released
|
|
4291
|
+
-- and expired are on the feed). Rows from before keep 'cancelled', as every one of them was. A late
|
|
4292
|
+
-- cancellation's fee (B5), recorded for the integrator to charge, and an admin's reason for going
|
|
4293
|
+
-- past a cutoff.
|
|
4294
|
+
ALTER TABLE slotlock.reservation_tombstones
|
|
4295
|
+
ADD COLUMN IF NOT EXISTS end_reason text NOT NULL DEFAULT 'cancelled';
|
|
4296
|
+
ALTER TABLE slotlock.reservation_tombstones ADD COLUMN IF NOT EXISTS cancellation_fee jsonb;
|
|
4297
|
+
ALTER TABLE slotlock.reservation_tombstones ADD COLUMN IF NOT EXISTS override_reason text;
|
|
4298
|
+
DO $slotlock_tombstone_end_reason$
|
|
4299
|
+
BEGIN
|
|
4300
|
+
IF NOT EXISTS (
|
|
4301
|
+
SELECT 1 FROM pg_catalog.pg_constraint
|
|
4302
|
+
WHERE conrelid = 'slotlock.reservation_tombstones'::regclass
|
|
4303
|
+
AND conname = 'slotlock_reservation_tombstones_end_valid'
|
|
4304
|
+
) THEN
|
|
4305
|
+
ALTER TABLE slotlock.reservation_tombstones
|
|
4306
|
+
ADD CONSTRAINT slotlock_reservation_tombstones_end_valid CHECK (
|
|
4307
|
+
end_reason IN ('cancelled', 'no_show', 'released', 'expired', 'erased')
|
|
4308
|
+
AND (cancellation_fee IS NULL OR (jsonb_typeof(cancellation_fee) = 'object'
|
|
4309
|
+
AND octet_length(cancellation_fee::text) <= 200))
|
|
4310
|
+
AND (override_reason IS NULL OR char_length(override_reason) BETWEEN 1 AND 500)
|
|
4311
|
+
);
|
|
4312
|
+
END IF;
|
|
4313
|
+
END
|
|
4314
|
+
$slotlock_tombstone_end_reason$;
|
|
4081
4315
|
-- Resource identity: one resource per tenant/external_ref namespace (plus one isolated NULL-tenant
|
|
4082
4316
|
-- legacy namespace). Dedupe first (repoint reservations to the keeper, drop the rest) so the
|
|
4083
4317
|
-- unique indexes also bind on DBs poisoned by the pre-index
|
|
@@ -4755,6 +4989,9 @@ CREATE TABLE IF NOT EXISTS slotlock.approval_policies (
|
|
|
4755
4989
|
AND lead_minutes BETWEEN 0 AND 43200
|
|
4756
4990
|
)
|
|
4757
4991
|
);
|
|
4992
|
+
-- A policy holds agents' bookings, and the tenant's own through the application API (bundles and
|
|
4993
|
+
-- pool bookings) only when it includes them (RFC 0004 B7).
|
|
4994
|
+
ALTER TABLE slotlock.approval_policies ADD COLUMN IF NOT EXISTS include_application boolean NOT NULL DEFAULT false;
|
|
4758
4995
|
|
|
4759
4996
|
-- A write waiting for a person: its time held by holds whose holder is the approval (never the
|
|
4760
4997
|
-- agent, so it cannot release them and they do not count against its caps), and the request to run
|
|
@@ -4799,6 +5036,147 @@ CREATE INDEX IF NOT EXISTS slotlock_approvals_pending_idx
|
|
|
4799
5036
|
CREATE INDEX IF NOT EXISTS slotlock_approvals_expiry_idx
|
|
4800
5037
|
ON slotlock.approvals (expires_at) WHERE status = 'pending';
|
|
4801
5038
|
|
|
5039
|
+
-- Stored hours (RFC 0004 B2): the tenant's (no location) and each location's. A resource without
|
|
5040
|
+
-- its own inherits its location's, the nearest up the chain, then its tenant's, then the server's.
|
|
5041
|
+
-- Rules are written validated (weekly rules, at most 50).
|
|
5042
|
+
CREATE TABLE IF NOT EXISTS slotlock.availability_defaults (
|
|
5043
|
+
tenant_ref text NOT NULL,
|
|
5044
|
+
location_ref text,
|
|
5045
|
+
rules jsonb NOT NULL,
|
|
5046
|
+
updated_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
|
5047
|
+
CONSTRAINT slotlock_availability_defaults_location_fkey FOREIGN KEY (tenant_ref, location_ref)
|
|
5048
|
+
REFERENCES slotlock.locations (tenant_ref, ref) ON DELETE CASCADE,
|
|
5049
|
+
CONSTRAINT slotlock_availability_defaults_valid CHECK (
|
|
5050
|
+
octet_length(tenant_ref) BETWEEN 1 AND 500
|
|
5051
|
+
AND jsonb_typeof(rules) = 'array' AND jsonb_array_length(rules) <= 50
|
|
5052
|
+
AND octet_length(rules::text) <= 65536
|
|
5053
|
+
)
|
|
5054
|
+
);
|
|
5055
|
+
CREATE UNIQUE INDEX IF NOT EXISTS slotlock_availability_defaults_scope_idx
|
|
5056
|
+
ON slotlock.availability_defaults (tenant_ref, (COALESCE(location_ref, '')));
|
|
5057
|
+
|
|
5058
|
+
-- Exceptions to the weekly hours (RFC 0004 B1): whole local days, closed or open with hours of
|
|
5059
|
+
-- their own, for the tenant, a location (and every resource inside it) or one resource. An import
|
|
5060
|
+
-- names its source's id, so importing again updates the same row.
|
|
5061
|
+
CREATE TABLE IF NOT EXISTS slotlock.availability_exceptions (
|
|
5062
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
5063
|
+
tenant_ref text NOT NULL,
|
|
5064
|
+
location_ref text,
|
|
5065
|
+
resource_id uuid,
|
|
5066
|
+
starts_on date NOT NULL,
|
|
5067
|
+
ends_on date NOT NULL,
|
|
5068
|
+
outcome text NOT NULL,
|
|
5069
|
+
hours jsonb,
|
|
5070
|
+
label text NOT NULL,
|
|
5071
|
+
source text NOT NULL DEFAULT 'manual',
|
|
5072
|
+
source_uid text,
|
|
5073
|
+
created_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
|
5074
|
+
CONSTRAINT slotlock_availability_exceptions_location_fkey FOREIGN KEY (tenant_ref, location_ref)
|
|
5075
|
+
REFERENCES slotlock.locations (tenant_ref, ref) ON DELETE CASCADE,
|
|
5076
|
+
CONSTRAINT slotlock_availability_exceptions_resource_fkey FOREIGN KEY (resource_id, tenant_ref)
|
|
5077
|
+
REFERENCES slotlock.resources (id, tenant_ref) ON DELETE CASCADE,
|
|
5078
|
+
CONSTRAINT slotlock_availability_exceptions_valid CHECK (
|
|
5079
|
+
octet_length(tenant_ref) BETWEEN 1 AND 500
|
|
5080
|
+
AND (location_ref IS NULL OR resource_id IS NULL)
|
|
5081
|
+
AND ends_on >= starts_on AND ends_on - starts_on < 367
|
|
5082
|
+
AND outcome IN ('closed', 'open')
|
|
5083
|
+
AND ((outcome = 'open') = (hours IS NOT NULL))
|
|
5084
|
+
AND (hours IS NULL OR (jsonb_typeof(hours) = 'array' AND jsonb_array_length(hours) BETWEEN 1 AND 4))
|
|
5085
|
+
AND char_length(label) BETWEEN 1 AND 200 AND btrim(label) <> ''
|
|
5086
|
+
AND source IN ('manual', 'ics', 'vavailability')
|
|
5087
|
+
AND (source_uid IS NULL OR octet_length(source_uid) BETWEEN 1 AND 600)
|
|
5088
|
+
)
|
|
5089
|
+
);
|
|
5090
|
+
CREATE UNIQUE INDEX IF NOT EXISTS slotlock_availability_exceptions_source_idx
|
|
5091
|
+
ON slotlock.availability_exceptions
|
|
5092
|
+
(tenant_ref, (COALESCE(location_ref, '')), (COALESCE(resource_id::text, '')), source_uid)
|
|
5093
|
+
WHERE source_uid IS NOT NULL;
|
|
5094
|
+
CREATE INDEX IF NOT EXISTS slotlock_availability_exceptions_days_idx
|
|
5095
|
+
ON slotlock.availability_exceptions (tenant_ref, ends_on, starts_on);
|
|
5096
|
+
CREATE INDEX IF NOT EXISTS slotlock_availability_exceptions_resource_idx
|
|
5097
|
+
ON slotlock.availability_exceptions (resource_id) WHERE resource_id IS NOT NULL;
|
|
5098
|
+
|
|
5099
|
+
-- A holiday calendar imported into closures (RFC 0004 B1), for the tenant, a location or a
|
|
5100
|
+
-- resource, by name: its closures name it, so importing it again removes the holidays it has
|
|
5101
|
+
-- dropped. With a URL it is a subscription, which maintenance imports again by next_import_at.
|
|
5102
|
+
-- Deleting it deletes its closures.
|
|
5103
|
+
CREATE TABLE IF NOT EXISTS slotlock.holiday_calendars (
|
|
5104
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
5105
|
+
tenant_ref text NOT NULL,
|
|
5106
|
+
location_ref text,
|
|
5107
|
+
resource_id uuid,
|
|
5108
|
+
name text NOT NULL,
|
|
5109
|
+
url text,
|
|
5110
|
+
imported_at timestamptz,
|
|
5111
|
+
next_import_at timestamptz,
|
|
5112
|
+
last_error text,
|
|
5113
|
+
created_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
|
5114
|
+
CONSTRAINT slotlock_holiday_calendars_tenant_key UNIQUE (id, tenant_ref),
|
|
5115
|
+
CONSTRAINT slotlock_holiday_calendars_location_fkey FOREIGN KEY (tenant_ref, location_ref)
|
|
5116
|
+
REFERENCES slotlock.locations (tenant_ref, ref) ON DELETE CASCADE,
|
|
5117
|
+
CONSTRAINT slotlock_holiday_calendars_resource_fkey FOREIGN KEY (resource_id, tenant_ref)
|
|
5118
|
+
REFERENCES slotlock.resources (id, tenant_ref) ON DELETE CASCADE,
|
|
5119
|
+
CONSTRAINT slotlock_holiday_calendars_valid CHECK (
|
|
5120
|
+
octet_length(tenant_ref) BETWEEN 1 AND 500
|
|
5121
|
+
AND (location_ref IS NULL OR resource_id IS NULL)
|
|
5122
|
+
AND char_length(name) BETWEEN 1 AND 200 AND btrim(name) <> ''
|
|
5123
|
+
AND (url IS NULL OR (url ~ '^https://' AND octet_length(url) <= 2048))
|
|
5124
|
+
AND (next_import_at IS NULL OR url IS NOT NULL)
|
|
5125
|
+
AND (last_error IS NULL OR char_length(last_error) <= 500)
|
|
5126
|
+
)
|
|
5127
|
+
);
|
|
5128
|
+
CREATE UNIQUE INDEX IF NOT EXISTS slotlock_holiday_calendars_name_idx
|
|
5129
|
+
ON slotlock.holiday_calendars (tenant_ref, (COALESCE(location_ref, '')), (COALESCE(resource_id::text, '')), name);
|
|
5130
|
+
CREATE INDEX IF NOT EXISTS slotlock_holiday_calendars_due_idx
|
|
5131
|
+
ON slotlock.holiday_calendars (tenant_ref, next_import_at) WHERE next_import_at IS NOT NULL;
|
|
5132
|
+
-- The calendar an imported closure came from: NULL for one added by hand, or imported before
|
|
5133
|
+
-- calendars were kept (claimed by the next import of a calendar that names it).
|
|
5134
|
+
ALTER TABLE slotlock.availability_exceptions ADD COLUMN IF NOT EXISTS calendar_id uuid;
|
|
5135
|
+
DO $slotlock_exception_calendar$
|
|
5136
|
+
BEGIN
|
|
5137
|
+
IF NOT EXISTS (
|
|
5138
|
+
SELECT 1 FROM pg_constraint
|
|
5139
|
+
WHERE conname = 'slotlock_availability_exceptions_calendar_fkey'
|
|
5140
|
+
AND conrelid = 'slotlock.availability_exceptions'::regclass
|
|
5141
|
+
) THEN
|
|
5142
|
+
ALTER TABLE slotlock.availability_exceptions
|
|
5143
|
+
ADD CONSTRAINT slotlock_availability_exceptions_calendar_fkey FOREIGN KEY (calendar_id, tenant_ref)
|
|
5144
|
+
REFERENCES slotlock.holiday_calendars (id, tenant_ref) ON DELETE CASCADE;
|
|
5145
|
+
END IF;
|
|
5146
|
+
END
|
|
5147
|
+
$slotlock_exception_calendar$;
|
|
5148
|
+
CREATE INDEX IF NOT EXISTS slotlock_availability_exceptions_calendar_idx
|
|
5149
|
+
ON slotlock.availability_exceptions (calendar_id) WHERE calendar_id IS NOT NULL;
|
|
5150
|
+
|
|
5151
|
+
-- The hours a resource without its own inherits: its location's, the nearest up the chain, else its
|
|
5152
|
+
-- tenant's, as {source, location?, rules}; NULL leaves the server's default. Runs as the caller, so
|
|
5153
|
+
-- row-level security keeps it inside the tenant.
|
|
5154
|
+
CREATE OR REPLACE FUNCTION slotlock.inherited_availability(requested_tenant_ref text, requested_location_ref text)
|
|
5155
|
+
RETURNS jsonb
|
|
5156
|
+
LANGUAGE sql
|
|
5157
|
+
STABLE
|
|
5158
|
+
SET search_path = pg_catalog, slotlock, pg_temp
|
|
5159
|
+
AS $slotlock_inherited_availability$
|
|
5160
|
+
WITH RECURSIVE up(ref, parent_ref, steps) AS (
|
|
5161
|
+
SELECT place.ref, place.parent_ref, 1 FROM slotlock.locations place
|
|
5162
|
+
WHERE requested_location_ref IS NOT NULL
|
|
5163
|
+
AND place.tenant_ref = requested_tenant_ref AND place.ref = requested_location_ref
|
|
5164
|
+
UNION ALL
|
|
5165
|
+
SELECT place.ref, place.parent_ref, up.steps + 1
|
|
5166
|
+
FROM slotlock.locations place JOIN up ON place.tenant_ref = requested_tenant_ref AND place.ref = up.parent_ref
|
|
5167
|
+
WHERE up.steps <= ${SLOTLOCK_MAX_LOCATION_DEPTH}
|
|
5168
|
+
)
|
|
5169
|
+
SELECT COALESCE(
|
|
5170
|
+
(SELECT jsonb_build_object('source', 'location', 'location', up.ref, 'rules', defaults.rules)
|
|
5171
|
+
FROM up JOIN slotlock.availability_defaults defaults
|
|
5172
|
+
ON defaults.tenant_ref = requested_tenant_ref AND defaults.location_ref = up.ref
|
|
5173
|
+
ORDER BY up.steps LIMIT 1),
|
|
5174
|
+
(SELECT jsonb_build_object('source', 'tenant', 'rules', defaults.rules)
|
|
5175
|
+
FROM slotlock.availability_defaults defaults
|
|
5176
|
+
WHERE defaults.tenant_ref = requested_tenant_ref AND defaults.location_ref IS NULL)
|
|
5177
|
+
);
|
|
5178
|
+
$slotlock_inherited_availability$;
|
|
5179
|
+
|
|
4802
5180
|
-- Every row occupies from its start to its occupied end: its end plus its turnaround buffer. Up to
|
|
4803
5181
|
-- 0.1 the buffer bound only tenant-owned bookings to each other (slotlock_reservations_buffered_no_overlap),
|
|
4804
5182
|
-- so a hold could take a booking's buffer and a buffered booking could cover a hold. The upgrade
|
|
@@ -5075,6 +5453,22 @@ ALTER TABLE slotlock.calendar_events ADD COLUMN IF NOT EXISTS recurrence_ends_by
|
|
|
5075
5453
|
ALTER TABLE slotlock.calendar_events
|
|
5076
5454
|
ADD COLUMN IF NOT EXISTS buffer_after_ms integer NOT NULL DEFAULT 0
|
|
5077
5455
|
CONSTRAINT slotlock_calendar_events_buffer_valid CHECK (buffer_after_ms BETWEEN 0 AND 86400000);
|
|
5456
|
+
-- A turnaround may run to a week (RFC 0004 B3): the wider check admits every row the old one did,
|
|
5457
|
+
-- so it is added NOT VALID (no scan under the lock), and the old one dropped.
|
|
5458
|
+
DO $slotlock_calendar_event_week_turnaround$
|
|
5459
|
+
BEGIN
|
|
5460
|
+
IF NOT EXISTS (
|
|
5461
|
+
SELECT 1 FROM pg_catalog.pg_constraint
|
|
5462
|
+
WHERE conrelid = 'slotlock.calendar_events'::regclass
|
|
5463
|
+
AND conname = 'slotlock_calendar_events_buffer_week_valid'
|
|
5464
|
+
) THEN
|
|
5465
|
+
ALTER TABLE slotlock.calendar_events
|
|
5466
|
+
ADD CONSTRAINT slotlock_calendar_events_buffer_week_valid
|
|
5467
|
+
CHECK (buffer_after_ms BETWEEN 0 AND 604800000) NOT VALID;
|
|
5468
|
+
END IF;
|
|
5469
|
+
ALTER TABLE slotlock.calendar_events DROP CONSTRAINT IF EXISTS slotlock_calendar_events_buffer_valid;
|
|
5470
|
+
END
|
|
5471
|
+
$slotlock_calendar_event_week_turnaround$;
|
|
5078
5472
|
-- The leading buffer a booked event keeps before each occurrence, kept for a later roll alike.
|
|
5079
5473
|
ALTER TABLE slotlock.calendar_events
|
|
5080
5474
|
ADD COLUMN IF NOT EXISTS buffer_before_ms integer NOT NULL DEFAULT 0
|
|
@@ -5398,6 +5792,205 @@ CREATE TABLE IF NOT EXISTS slotlock.calendar_event_occurrences (
|
|
|
5398
5792
|
)
|
|
5399
5793
|
);
|
|
5400
5794
|
|
|
5795
|
+
-- Zone rule changes reach everything (RFC 0004 B9): a series' occurrences, and reservations that
|
|
5796
|
+
-- carry the wall time they were booked for, keep it, so maintenance finds those whose wall time now
|
|
5797
|
+
-- falls at another instant. An occurrence's is its series' zone's wall time when written; a
|
|
5798
|
+
-- reservation's is what the writer gave (local_start), in its resource's zone.
|
|
5799
|
+
ALTER TABLE slotlock.calendar_event_occurrences ADD COLUMN IF NOT EXISTS wall_starts_at timestamp;
|
|
5800
|
+
ALTER TABLE slotlock.calendar_event_occurrences ADD COLUMN IF NOT EXISTS zone_shift_reported_at timestamptz;
|
|
5801
|
+
CREATE OR REPLACE FUNCTION slotlock.calendar_event_occurrence_wall_time()
|
|
5802
|
+
RETURNS trigger LANGUAGE plpgsql SET search_path = pg_catalog, slotlock, pg_temp
|
|
5803
|
+
AS $slotlock_occurrence_wall_time$
|
|
5804
|
+
BEGIN
|
|
5805
|
+
IF TG_OP = 'INSERT' OR NEW.starts_at IS DISTINCT FROM OLD.starts_at THEN
|
|
5806
|
+
NEW.wall_starts_at := slotlock.zoned_wall(
|
|
5807
|
+
NEW.starts_at, (SELECT event.timezone FROM slotlock.calendar_events event WHERE event.id = NEW.event_id));
|
|
5808
|
+
NEW.zone_shift_reported_at := NULL;
|
|
5809
|
+
END IF;
|
|
5810
|
+
RETURN NEW;
|
|
5811
|
+
END
|
|
5812
|
+
$slotlock_occurrence_wall_time$;
|
|
5813
|
+
DROP TRIGGER IF EXISTS slotlock_calendar_event_occurrence_wall_time ON slotlock.calendar_event_occurrences;
|
|
5814
|
+
CREATE TRIGGER slotlock_calendar_event_occurrence_wall_time
|
|
5815
|
+
BEFORE INSERT OR UPDATE ON slotlock.calendar_event_occurrences
|
|
5816
|
+
FOR EACH ROW EXECUTE FUNCTION slotlock.calendar_event_occurrence_wall_time();
|
|
5817
|
+
-- Occurrences to come written before this release: their wall time as the rules read now. A new
|
|
5818
|
+
-- database has none, and may not have slotlock.zoned_wall yet at this point.
|
|
5819
|
+
DO $slotlock_occurrence_wall_backfill$
|
|
5820
|
+
BEGIN
|
|
5821
|
+
IF to_regprocedure('slotlock.zoned_wall(timestamptz, text)') IS NOT NULL THEN
|
|
5822
|
+
EXECUTE $backfill$
|
|
5823
|
+
UPDATE slotlock.calendar_event_occurrences occurrence
|
|
5824
|
+
SET wall_starts_at = slotlock.zoned_wall(occurrence.starts_at, event.timezone)
|
|
5825
|
+
FROM slotlock.calendar_events event
|
|
5826
|
+
WHERE event.id = occurrence.event_id AND occurrence.wall_starts_at IS NULL
|
|
5827
|
+
AND occurrence.starts_at > now()
|
|
5828
|
+
$backfill$;
|
|
5829
|
+
END IF;
|
|
5830
|
+
END
|
|
5831
|
+
$slotlock_occurrence_wall_backfill$;
|
|
5832
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS wall_starts_at timestamp;
|
|
5833
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS zone_shift_reported_at timestamptz;
|
|
5834
|
+
|
|
5835
|
+
-- Integrations (RFC 0004 P3). Core never sends a message and never holds a provider token; it keeps
|
|
5836
|
+
-- what the packages that do need. A booking may name the person to tell (contact_ref) and say not
|
|
5837
|
+
-- to tell anyone (notify); a hold may wait for the integrator's payment before it is confirmed.
|
|
5838
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS contact_ref text;
|
|
5839
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS notify boolean NOT NULL DEFAULT true;
|
|
5840
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS payment_required boolean NOT NULL DEFAULT false;
|
|
5841
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS payment_ref text;
|
|
5842
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS paid_at timestamptz;
|
|
5843
|
+
-- What a hold's payment must be, and what was received (RFC 0004 C3), in the currency's minor unit.
|
|
5844
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS payment_amount_minor bigint;
|
|
5845
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS payment_currency text;
|
|
5846
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS paid_amount_minor bigint;
|
|
5847
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS paid_currency text;
|
|
5848
|
+
ALTER TABLE slotlock.calendar_events ADD COLUMN IF NOT EXISTS notify boolean NOT NULL DEFAULT true;
|
|
5849
|
+
DO $slotlock_integration_columns$
|
|
5850
|
+
BEGIN
|
|
5851
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'slotlock_reservations_integration_valid'
|
|
5852
|
+
AND conrelid = 'slotlock.reservations'::regclass) THEN
|
|
5853
|
+
ALTER TABLE slotlock.reservations ADD CONSTRAINT slotlock_reservations_integration_valid CHECK (
|
|
5854
|
+
(contact_ref IS NULL OR contact_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$')
|
|
5855
|
+
AND (payment_ref IS NULL OR octet_length(payment_ref) BETWEEN 1 AND 255)
|
|
5856
|
+
AND (paid_at IS NULL OR payment_ref IS NOT NULL)
|
|
5857
|
+
) NOT VALID;
|
|
5858
|
+
END IF;
|
|
5859
|
+
END
|
|
5860
|
+
$slotlock_integration_columns$;
|
|
5861
|
+
DO $slotlock_payment_amounts$
|
|
5862
|
+
BEGIN
|
|
5863
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'slotlock_reservations_payment_amounts_valid'
|
|
5864
|
+
AND conrelid = 'slotlock.reservations'::regclass) THEN
|
|
5865
|
+
ALTER TABLE slotlock.reservations ADD CONSTRAINT slotlock_reservations_payment_amounts_valid CHECK (
|
|
5866
|
+
(payment_amount_minor IS NULL) = (payment_currency IS NULL)
|
|
5867
|
+
AND (paid_amount_minor IS NULL) = (paid_currency IS NULL)
|
|
5868
|
+
AND (payment_amount_minor IS NULL OR payment_amount_minor BETWEEN 0 AND 1000000000000)
|
|
5869
|
+
AND (paid_amount_minor IS NULL OR paid_amount_minor BETWEEN 0 AND 1000000000000)
|
|
5870
|
+
AND (payment_currency IS NULL OR payment_currency ~ '^[A-Z]{3}$')
|
|
5871
|
+
AND (paid_currency IS NULL OR paid_currency ~ '^[A-Z]{3}$')
|
|
5872
|
+
) NOT VALID;
|
|
5873
|
+
END IF;
|
|
5874
|
+
END
|
|
5875
|
+
$slotlock_payment_amounts$;
|
|
5876
|
+
-- Forgetting a person finds the bookings that name them.
|
|
5877
|
+
CREATE INDEX IF NOT EXISTS slotlock_reservations_contact_idx
|
|
5878
|
+
ON slotlock.reservations (tenant_ref, contact_ref) WHERE contact_ref IS NOT NULL;
|
|
5879
|
+
|
|
5880
|
+
-- The people a tenant's bookings may tell (RFC 0004 P3): how to reach each, and what each agreed
|
|
5881
|
+
-- to. A person is erased by deleting their row; bookings keep no copy of what it held.
|
|
5882
|
+
CREATE TABLE IF NOT EXISTS slotlock.contacts (
|
|
5883
|
+
tenant_ref text NOT NULL,
|
|
5884
|
+
ref text NOT NULL,
|
|
5885
|
+
name text,
|
|
5886
|
+
email text,
|
|
5887
|
+
phone text,
|
|
5888
|
+
whatsapp text,
|
|
5889
|
+
consent jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
5890
|
+
locale text,
|
|
5891
|
+
created_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
|
5892
|
+
updated_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
|
5893
|
+
PRIMARY KEY (tenant_ref, ref),
|
|
5894
|
+
CONSTRAINT slotlock_contacts_valid CHECK (
|
|
5895
|
+
octet_length(tenant_ref) BETWEEN 1 AND 500
|
|
5896
|
+
AND ref ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$'
|
|
5897
|
+
AND (name IS NULL OR char_length(name) BETWEEN 1 AND 200)
|
|
5898
|
+
AND (email IS NULL OR (char_length(email) BETWEEN 3 AND 320 AND email LIKE '%_@_%'))
|
|
5899
|
+
AND (phone IS NULL OR phone ~ '^[+][1-9][0-9]{6,14}$')
|
|
5900
|
+
AND (whatsapp IS NULL OR whatsapp ~ '^[+][1-9][0-9]{6,14}$')
|
|
5901
|
+
AND jsonb_typeof(consent) = 'object' AND octet_length(consent::text) <= 512
|
|
5902
|
+
AND (locale IS NULL OR locale ~ '^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$')
|
|
5903
|
+
)
|
|
5904
|
+
);
|
|
5905
|
+
|
|
5906
|
+
-- Each reminder once (RFC 0004 P3): when maintenance put reminder.due on the feed for an
|
|
5907
|
+
-- occurrence, so a later pass never says it again.
|
|
5908
|
+
CREATE TABLE IF NOT EXISTS slotlock.reminder_dispatches (
|
|
5909
|
+
tenant_ref text NOT NULL,
|
|
5910
|
+
event_id uuid NOT NULL,
|
|
5911
|
+
recurrence_id text NOT NULL,
|
|
5912
|
+
ordinal integer NOT NULL,
|
|
5913
|
+
due_at timestamptz NOT NULL,
|
|
5914
|
+
dispatched_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
|
5915
|
+
PRIMARY KEY (event_id, recurrence_id, ordinal),
|
|
5916
|
+
CONSTRAINT slotlock_reminder_dispatches_event_fkey FOREIGN KEY (event_id, tenant_ref)
|
|
5917
|
+
REFERENCES slotlock.calendar_events (id, tenant_ref) ON DELETE CASCADE
|
|
5918
|
+
);
|
|
5919
|
+
CREATE INDEX IF NOT EXISTS slotlock_reminder_dispatches_tenant_idx
|
|
5920
|
+
ON slotlock.reminder_dispatches (tenant_ref, due_at);
|
|
5921
|
+
|
|
5922
|
+
-- A booking's reminders (RFC 0004 P3): minutes before its start, at most five; each said once on
|
|
5923
|
+
-- the feed as reminder.due, and again if the booking moves.
|
|
5924
|
+
ALTER TABLE slotlock.reservations ADD COLUMN IF NOT EXISTS reminder_minutes integer[];
|
|
5925
|
+
DO $slotlock_reservation_reminders$
|
|
5926
|
+
BEGIN
|
|
5927
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'slotlock_reservations_reminders_valid'
|
|
5928
|
+
AND conrelid = 'slotlock.reservations'::regclass) THEN
|
|
5929
|
+
ALTER TABLE slotlock.reservations ADD CONSTRAINT slotlock_reservations_reminders_valid CHECK (
|
|
5930
|
+
reminder_minutes IS NULL
|
|
5931
|
+
OR (cardinality(reminder_minutes) BETWEEN 1 AND 5
|
|
5932
|
+
AND 0 <= ALL (reminder_minutes) AND 527040 >= ALL (reminder_minutes))
|
|
5933
|
+
) NOT VALID;
|
|
5934
|
+
END IF;
|
|
5935
|
+
END
|
|
5936
|
+
$slotlock_reservation_reminders$;
|
|
5937
|
+
CREATE TABLE IF NOT EXISTS slotlock.reservation_reminder_dispatches (
|
|
5938
|
+
tenant_ref text NOT NULL,
|
|
5939
|
+
reservation_id uuid NOT NULL,
|
|
5940
|
+
ordinal integer NOT NULL CHECK (ordinal BETWEEN 0 AND 4),
|
|
5941
|
+
due_at timestamptz NOT NULL,
|
|
5942
|
+
dispatched_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
|
5943
|
+
PRIMARY KEY (reservation_id, ordinal),
|
|
5944
|
+
CONSTRAINT slotlock_reservation_reminder_dispatches_fkey FOREIGN KEY (reservation_id, tenant_ref)
|
|
5945
|
+
REFERENCES slotlock.reservations (id, tenant_ref) ON DELETE CASCADE
|
|
5946
|
+
);
|
|
5947
|
+
CREATE INDEX IF NOT EXISTS slotlock_reservation_reminder_dispatches_tenant_idx
|
|
5948
|
+
ON slotlock.reservation_reminder_dispatches (tenant_ref, due_at);
|
|
5949
|
+
|
|
5950
|
+
-- Each hold's payment, kept once recorded (RFC 0004 C3), however the hold ends: confirmed in place,
|
|
5951
|
+
-- booked as an event (its row deleted), released or expired. So the same payment, told again later,
|
|
5952
|
+
-- is answered as the payment already recorded, never as one to refund.
|
|
5953
|
+
CREATE TABLE IF NOT EXISTS slotlock.hold_payments (
|
|
5954
|
+
tenant_ref text NOT NULL,
|
|
5955
|
+
hold_id uuid PRIMARY KEY,
|
|
5956
|
+
resource_id uuid NOT NULL,
|
|
5957
|
+
starts_at timestamptz NOT NULL,
|
|
5958
|
+
ends_at timestamptz NOT NULL,
|
|
5959
|
+
buffer_after_ms bigint NOT NULL DEFAULT 0,
|
|
5960
|
+
expires_at timestamptz NOT NULL,
|
|
5961
|
+
payment_ref text NOT NULL CHECK (octet_length(payment_ref) BETWEEN 1 AND 255),
|
|
5962
|
+
paid_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
|
5963
|
+
payment_amount_minor bigint,
|
|
5964
|
+
payment_currency text,
|
|
5965
|
+
paid_amount_minor bigint,
|
|
5966
|
+
paid_currency text,
|
|
5967
|
+
CONSTRAINT slotlock_hold_payments_resource_fkey FOREIGN KEY (resource_id, tenant_ref)
|
|
5968
|
+
REFERENCES slotlock.resources (id, tenant_ref) ON DELETE CASCADE
|
|
5969
|
+
);
|
|
5970
|
+
-- When each channel's consent was last given or withdrawn, as proof of it.
|
|
5971
|
+
ALTER TABLE slotlock.contacts ADD COLUMN IF NOT EXISTS consent_at jsonb NOT NULL DEFAULT '{}'::jsonb;
|
|
5972
|
+
|
|
5973
|
+
-- Busy time a calendar reports without its events (Google freeBusy, Microsoft getSchedule), for one
|
|
5974
|
+
-- resource and source (RFC 0004 C1): replaced window by window with the source's coverage, counted
|
|
5975
|
+
-- in free/busy and refused to every booking Slotlock makes, as an event's time would be.
|
|
5976
|
+
CREATE TABLE IF NOT EXISTS slotlock.source_busy (
|
|
5977
|
+
tenant_ref text NOT NULL,
|
|
5978
|
+
resource_id uuid NOT NULL,
|
|
5979
|
+
source text NOT NULL,
|
|
5980
|
+
starts_at timestamptz NOT NULL,
|
|
5981
|
+
ends_at timestamptz NOT NULL,
|
|
5982
|
+
CONSTRAINT slotlock_source_busy_resource_fkey FOREIGN KEY (resource_id, tenant_ref)
|
|
5983
|
+
REFERENCES slotlock.resources (id, tenant_ref) ON DELETE CASCADE,
|
|
5984
|
+
CONSTRAINT slotlock_source_busy_valid CHECK (
|
|
5985
|
+
octet_length(tenant_ref) BETWEEN 1 AND 500 AND octet_length(source) BETWEEN 1 AND 500
|
|
5986
|
+
AND ends_at > starts_at
|
|
5987
|
+
)
|
|
5988
|
+
);
|
|
5989
|
+
CREATE INDEX IF NOT EXISTS slotlock_source_busy_resource_idx
|
|
5990
|
+
ON slotlock.source_busy (resource_id, starts_at, ends_at);
|
|
5991
|
+
CREATE INDEX IF NOT EXISTS slotlock_source_busy_source_idx
|
|
5992
|
+
ON slotlock.source_busy (resource_id, source, starts_at);
|
|
5993
|
+
|
|
5401
5994
|
-- A cancelled UID remains terminal. This blocks delayed create delivery after the event row and
|
|
5402
5995
|
-- its derived reservations have been removed.
|
|
5403
5996
|
CREATE TABLE IF NOT EXISTS slotlock.calendar_event_tombstones (
|
|
@@ -5742,6 +6335,7 @@ export const SLOTLOCK_TENANT_TABLES = Object.freeze([
|
|
|
5742
6335
|
'booking_rule_defaults',
|
|
5743
6336
|
'assistant_settings',
|
|
5744
6337
|
'assistant_conversations',
|
|
6338
|
+
'assistant_chats',
|
|
5745
6339
|
'locations',
|
|
5746
6340
|
'resource_groups',
|
|
5747
6341
|
'resource_group_members',
|
|
@@ -5749,6 +6343,14 @@ export const SLOTLOCK_TENANT_TABLES = Object.freeze([
|
|
|
5749
6343
|
'composite_claims',
|
|
5750
6344
|
'approval_policies',
|
|
5751
6345
|
'approvals',
|
|
6346
|
+
'availability_defaults',
|
|
6347
|
+
'availability_exceptions',
|
|
6348
|
+
'holiday_calendars',
|
|
6349
|
+
'contacts',
|
|
6350
|
+
'reminder_dispatches',
|
|
6351
|
+
'reservation_reminder_dispatches',
|
|
6352
|
+
'hold_payments',
|
|
6353
|
+
'source_busy',
|
|
5752
6354
|
]);
|
|
5753
6355
|
const POSTGRES_SETTING_NAME = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$/;
|
|
5754
6356
|
const POSTGRES_ROLE_NAME = /^[a-z_][a-z0-9_]{0,62}$/;
|
|
@@ -5797,6 +6399,11 @@ export const SLOTLOCK_RESOURCE_SCOPED_TABLES = Object.freeze({
|
|
|
5797
6399
|
// A change or a waitlist entry about no one resource (a key, a pool) is not the key's.
|
|
5798
6400
|
outbox: resourceScopeAllows('resource_id'),
|
|
5799
6401
|
waitlist_entries: resourceScopeAllows('resource_id'),
|
|
6402
|
+
// The tenant's and locations' exceptions shape every resource's hours, so every key reads them.
|
|
6403
|
+
availability_exceptions: `(resource_id IS NULL OR ${resourceScopeAllows('resource_id')})`,
|
|
6404
|
+
holiday_calendars: `(resource_id IS NULL OR ${resourceScopeAllows('resource_id')})`,
|
|
6405
|
+
source_busy: resourceScopeAllows('resource_id'),
|
|
6406
|
+
hold_payments: resourceScopeAllows('resource_id'),
|
|
5800
6407
|
...Object.fromEntries([
|
|
5801
6408
|
'calendar_event_attendees',
|
|
5802
6409
|
'calendar_event_reminders',
|
|
@@ -5862,6 +6469,8 @@ ALTER TABLE slotlock.assistant_settings ENABLE ROW LEVEL SECURITY;
|
|
|
5862
6469
|
ALTER TABLE slotlock.assistant_settings FORCE ROW LEVEL SECURITY;
|
|
5863
6470
|
ALTER TABLE slotlock.assistant_conversations ENABLE ROW LEVEL SECURITY;
|
|
5864
6471
|
ALTER TABLE slotlock.assistant_conversations FORCE ROW LEVEL SECURITY;
|
|
6472
|
+
ALTER TABLE slotlock.assistant_chats ENABLE ROW LEVEL SECURITY;
|
|
6473
|
+
ALTER TABLE slotlock.assistant_chats FORCE ROW LEVEL SECURITY;
|
|
5865
6474
|
ALTER TABLE slotlock.locations ENABLE ROW LEVEL SECURITY;
|
|
5866
6475
|
ALTER TABLE slotlock.locations FORCE ROW LEVEL SECURITY;
|
|
5867
6476
|
ALTER TABLE slotlock.resource_groups ENABLE ROW LEVEL SECURITY;
|
|
@@ -5876,6 +6485,22 @@ ALTER TABLE slotlock.approval_policies ENABLE ROW LEVEL SECURITY;
|
|
|
5876
6485
|
ALTER TABLE slotlock.approval_policies FORCE ROW LEVEL SECURITY;
|
|
5877
6486
|
ALTER TABLE slotlock.approvals ENABLE ROW LEVEL SECURITY;
|
|
5878
6487
|
ALTER TABLE slotlock.approvals FORCE ROW LEVEL SECURITY;
|
|
6488
|
+
ALTER TABLE slotlock.availability_defaults ENABLE ROW LEVEL SECURITY;
|
|
6489
|
+
ALTER TABLE slotlock.availability_defaults FORCE ROW LEVEL SECURITY;
|
|
6490
|
+
ALTER TABLE slotlock.availability_exceptions ENABLE ROW LEVEL SECURITY;
|
|
6491
|
+
ALTER TABLE slotlock.availability_exceptions FORCE ROW LEVEL SECURITY;
|
|
6492
|
+
ALTER TABLE slotlock.contacts ENABLE ROW LEVEL SECURITY;
|
|
6493
|
+
ALTER TABLE slotlock.contacts FORCE ROW LEVEL SECURITY;
|
|
6494
|
+
ALTER TABLE slotlock.reminder_dispatches ENABLE ROW LEVEL SECURITY;
|
|
6495
|
+
ALTER TABLE slotlock.reminder_dispatches FORCE ROW LEVEL SECURITY;
|
|
6496
|
+
ALTER TABLE slotlock.reservation_reminder_dispatches ENABLE ROW LEVEL SECURITY;
|
|
6497
|
+
ALTER TABLE slotlock.reservation_reminder_dispatches FORCE ROW LEVEL SECURITY;
|
|
6498
|
+
ALTER TABLE slotlock.hold_payments ENABLE ROW LEVEL SECURITY;
|
|
6499
|
+
ALTER TABLE slotlock.hold_payments FORCE ROW LEVEL SECURITY;
|
|
6500
|
+
ALTER TABLE slotlock.holiday_calendars ENABLE ROW LEVEL SECURITY;
|
|
6501
|
+
ALTER TABLE slotlock.holiday_calendars FORCE ROW LEVEL SECURITY;
|
|
6502
|
+
ALTER TABLE slotlock.source_busy ENABLE ROW LEVEL SECURITY;
|
|
6503
|
+
ALTER TABLE slotlock.source_busy FORCE ROW LEVEL SECURITY;
|
|
5879
6504
|
|
|
5880
6505
|
-- One database must have one tenant-context contract. Re-applying with an adapter-specific
|
|
5881
6506
|
-- setting deliberately rebinds the stable policies instead of silently keeping a prior setting.
|
|
@@ -5908,13 +6533,22 @@ BEGIN
|
|
|
5908
6533
|
'booking_rule_defaults',
|
|
5909
6534
|
'assistant_settings',
|
|
5910
6535
|
'assistant_conversations',
|
|
6536
|
+
'assistant_chats',
|
|
5911
6537
|
'locations',
|
|
5912
6538
|
'resource_groups',
|
|
5913
6539
|
'resource_group_members',
|
|
5914
6540
|
'pool_settings',
|
|
5915
6541
|
'composite_claims',
|
|
5916
6542
|
'approval_policies',
|
|
5917
|
-
'approvals'
|
|
6543
|
+
'approvals',
|
|
6544
|
+
'availability_defaults',
|
|
6545
|
+
'availability_exceptions',
|
|
6546
|
+
'holiday_calendars',
|
|
6547
|
+
'contacts',
|
|
6548
|
+
'reminder_dispatches',
|
|
6549
|
+
'reservation_reminder_dispatches',
|
|
6550
|
+
'hold_payments',
|
|
6551
|
+
'source_busy'
|
|
5918
6552
|
)
|
|
5919
6553
|
LOOP
|
|
5920
6554
|
EXECUTE format(
|
|
@@ -6156,6 +6790,16 @@ BEGIN
|
|
|
6156
6790
|
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
6157
6791
|
END IF;
|
|
6158
6792
|
|
|
6793
|
+
IF NOT EXISTS (
|
|
6794
|
+
SELECT 1 FROM pg_policies
|
|
6795
|
+
WHERE schemaname = 'slotlock' AND tablename = 'assistant_chats'
|
|
6796
|
+
AND policyname = 'tenant_isolation_v1'
|
|
6797
|
+
) THEN
|
|
6798
|
+
CREATE POLICY tenant_isolation_v1 ON slotlock.assistant_chats
|
|
6799
|
+
USING (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''))
|
|
6800
|
+
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
6801
|
+
END IF;
|
|
6802
|
+
|
|
6159
6803
|
IF NOT EXISTS (
|
|
6160
6804
|
SELECT 1 FROM pg_policies
|
|
6161
6805
|
WHERE schemaname = 'slotlock' AND tablename = 'locations'
|
|
@@ -6225,6 +6869,86 @@ BEGIN
|
|
|
6225
6869
|
USING (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''))
|
|
6226
6870
|
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
6227
6871
|
END IF;
|
|
6872
|
+
|
|
6873
|
+
IF NOT EXISTS (
|
|
6874
|
+
SELECT 1 FROM pg_policies
|
|
6875
|
+
WHERE schemaname = 'slotlock' AND tablename = 'availability_defaults'
|
|
6876
|
+
AND policyname = 'tenant_isolation_v1'
|
|
6877
|
+
) THEN
|
|
6878
|
+
CREATE POLICY tenant_isolation_v1 ON slotlock.availability_defaults
|
|
6879
|
+
USING (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''))
|
|
6880
|
+
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
6881
|
+
END IF;
|
|
6882
|
+
|
|
6883
|
+
IF NOT EXISTS (
|
|
6884
|
+
SELECT 1 FROM pg_policies
|
|
6885
|
+
WHERE schemaname = 'slotlock' AND tablename = 'availability_exceptions'
|
|
6886
|
+
AND policyname = 'tenant_isolation_v1'
|
|
6887
|
+
) THEN
|
|
6888
|
+
CREATE POLICY tenant_isolation_v1 ON slotlock.availability_exceptions
|
|
6889
|
+
USING (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''))
|
|
6890
|
+
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
6891
|
+
END IF;
|
|
6892
|
+
|
|
6893
|
+
IF NOT EXISTS (
|
|
6894
|
+
SELECT 1 FROM pg_policies
|
|
6895
|
+
WHERE schemaname = 'slotlock' AND tablename = 'contacts'
|
|
6896
|
+
AND policyname = 'tenant_isolation_v1'
|
|
6897
|
+
) THEN
|
|
6898
|
+
CREATE POLICY tenant_isolation_v1 ON slotlock.contacts
|
|
6899
|
+
USING (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''))
|
|
6900
|
+
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
6901
|
+
END IF;
|
|
6902
|
+
|
|
6903
|
+
IF NOT EXISTS (
|
|
6904
|
+
SELECT 1 FROM pg_policies
|
|
6905
|
+
WHERE schemaname = 'slotlock' AND tablename = 'reminder_dispatches'
|
|
6906
|
+
AND policyname = 'tenant_isolation_v1'
|
|
6907
|
+
) THEN
|
|
6908
|
+
CREATE POLICY tenant_isolation_v1 ON slotlock.reminder_dispatches
|
|
6909
|
+
USING (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''))
|
|
6910
|
+
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
6911
|
+
END IF;
|
|
6912
|
+
|
|
6913
|
+
IF NOT EXISTS (
|
|
6914
|
+
SELECT 1 FROM pg_policies
|
|
6915
|
+
WHERE schemaname = 'slotlock' AND tablename = 'reservation_reminder_dispatches'
|
|
6916
|
+
AND policyname = 'tenant_isolation_v1'
|
|
6917
|
+
) THEN
|
|
6918
|
+
CREATE POLICY tenant_isolation_v1 ON slotlock.reservation_reminder_dispatches
|
|
6919
|
+
USING (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''))
|
|
6920
|
+
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
6921
|
+
END IF;
|
|
6922
|
+
|
|
6923
|
+
IF NOT EXISTS (
|
|
6924
|
+
SELECT 1 FROM pg_policies
|
|
6925
|
+
WHERE schemaname = 'slotlock' AND tablename = 'hold_payments'
|
|
6926
|
+
AND policyname = 'tenant_isolation_v1'
|
|
6927
|
+
) THEN
|
|
6928
|
+
CREATE POLICY tenant_isolation_v1 ON slotlock.hold_payments
|
|
6929
|
+
USING (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''))
|
|
6930
|
+
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
6931
|
+
END IF;
|
|
6932
|
+
|
|
6933
|
+
IF NOT EXISTS (
|
|
6934
|
+
SELECT 1 FROM pg_policies
|
|
6935
|
+
WHERE schemaname = 'slotlock' AND tablename = 'holiday_calendars'
|
|
6936
|
+
AND policyname = 'tenant_isolation_v1'
|
|
6937
|
+
) THEN
|
|
6938
|
+
CREATE POLICY tenant_isolation_v1 ON slotlock.holiday_calendars
|
|
6939
|
+
USING (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''))
|
|
6940
|
+
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
6941
|
+
END IF;
|
|
6942
|
+
|
|
6943
|
+
IF NOT EXISTS (
|
|
6944
|
+
SELECT 1 FROM pg_policies
|
|
6945
|
+
WHERE schemaname = 'slotlock' AND tablename = 'source_busy'
|
|
6946
|
+
AND policyname = 'tenant_isolation_v1'
|
|
6947
|
+
) THEN
|
|
6948
|
+
CREATE POLICY tenant_isolation_v1 ON slotlock.source_busy
|
|
6949
|
+
USING (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''))
|
|
6950
|
+
WITH CHECK (tenant_ref = NULLIF(current_setting('${tenantContextSetting}', true), ''));
|
|
6951
|
+
END IF;
|
|
6228
6952
|
END
|
|
6229
6953
|
$slotlock_rls$;
|
|
6230
6954
|
|
|
@@ -6268,6 +6992,8 @@ COMMENT ON POLICY tenant_isolation_v1 ON slotlock.assistant_settings
|
|
|
6268
6992
|
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
6269
6993
|
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.assistant_conversations
|
|
6270
6994
|
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
6995
|
+
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.assistant_chats
|
|
6996
|
+
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
6271
6997
|
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.locations
|
|
6272
6998
|
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
6273
6999
|
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.resource_groups
|
|
@@ -6282,6 +7008,22 @@ COMMENT ON POLICY tenant_isolation_v1 ON slotlock.approval_policies
|
|
|
6282
7008
|
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
6283
7009
|
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.approvals
|
|
6284
7010
|
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
7011
|
+
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.availability_defaults
|
|
7012
|
+
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
7013
|
+
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.availability_exceptions
|
|
7014
|
+
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
7015
|
+
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.contacts
|
|
7016
|
+
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
7017
|
+
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.reminder_dispatches
|
|
7018
|
+
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
7019
|
+
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.reservation_reminder_dispatches
|
|
7020
|
+
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
7021
|
+
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.hold_payments
|
|
7022
|
+
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
7023
|
+
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.holiday_calendars
|
|
7024
|
+
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
7025
|
+
COMMENT ON POLICY tenant_isolation_v1 ON slotlock.source_busy
|
|
7026
|
+
IS 'slotlock:tenant-context:${tenantContextSetting}';
|
|
6285
7027
|
${SLOTLOCK_RESOURCE_SCOPE_POLICIES}
|
|
6286
7028
|
|
|
6287
7029
|
INSERT INTO slotlock.rls_policy_contracts (
|
|
@@ -6326,13 +7068,22 @@ WHERE schemaname = 'slotlock'
|
|
|
6326
7068
|
'booking_rule_defaults',
|
|
6327
7069
|
'assistant_settings',
|
|
6328
7070
|
'assistant_conversations',
|
|
7071
|
+
'assistant_chats',
|
|
6329
7072
|
'locations',
|
|
6330
7073
|
'resource_groups',
|
|
6331
7074
|
'resource_group_members',
|
|
6332
7075
|
'pool_settings',
|
|
6333
7076
|
'composite_claims',
|
|
6334
7077
|
'approval_policies',
|
|
6335
|
-
'approvals'
|
|
7078
|
+
'approvals',
|
|
7079
|
+
'availability_defaults',
|
|
7080
|
+
'availability_exceptions',
|
|
7081
|
+
'holiday_calendars',
|
|
7082
|
+
'contacts',
|
|
7083
|
+
'reminder_dispatches',
|
|
7084
|
+
'reservation_reminder_dispatches',
|
|
7085
|
+
'hold_payments',
|
|
7086
|
+
'source_busy'
|
|
6336
7087
|
)
|
|
6337
7088
|
AND policyname = 'tenant_isolation_v1'
|
|
6338
7089
|
ON CONFLICT (table_name) DO UPDATE
|