@vrplatform/kysely 1.3.45-stage.6066 → 1.3.45-stage.607
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/build/.data.tar +0 -0
- package/build/main/control-plane/index.d.ts +312 -0
- package/build/main/control-plane/index.js.map +1 -1
- package/build/main/index.d.ts +13 -2
- package/build/main/index.js +217 -23
- package/build/main/index.js.map +1 -1
- package/build/main/local/generate.js +2 -0
- package/build/main/local/generate.js.map +1 -1
- package/build/main/local/index.js +110 -8
- package/build/main/local/index.js.map +1 -1
- package/build/main/postgres-error.d.ts +3 -3
- package/build/main/postgres-error.js +4 -3
- package/build/main/postgres-error.js.map +1 -1
- package/build/main/query/listing/status.js +2 -6
- package/build/main/query/listing/status.js.map +1 -1
- package/build/main/query/tenant/status.d.ts +1 -1
- package/build/main/query/tenant/status.js +6 -4
- package/build/main/query/tenant/status.js.map +1 -1
- package/build/main/regional.d.ts +1 -0
- package/build/main/regional.js +1 -0
- package/build/main/regional.js.map +1 -1
- package/build/main/test/index.d.ts +23 -3
- package/build/main/test/index.js +35 -13
- package/build/main/test/index.js.map +1 -1
- package/build/main/v1.generated.d.ts +560 -8
- package/build/main/v1.generated.js.map +1 -1
- package/build/migrations-v2/0000000000015-durable-flow-events.ts +163 -0
- package/build/migrations-v2/0000000000016-billing-listing-facts.ts +539 -0
- package/build/migrations-v2/0000000000016-managed-team-access.ts +34 -0
- package/build/migrations-v2/0000000000017-drop-trial-until.ts +42 -0
- package/build/migrations-v2/0000000000017-webhook-secret-reencryption-failures.ts +18 -0
- package/build/migrations-v2/0000000000018-accounting-integrity-monitoring.ts +276 -0
- package/build/migrations-v2/0000000000018-durable-flow-event-sensitive-input.ts +28 -0
- package/build/migrations-v2/0000000000019-accounting-integrity-run-shards.ts +26 -0
- package/build/migrations-v2/0000000000019-asynchronous-operations.ts +736 -0
- package/build/migrations-v2/0000000000020-accounting-integrity-case-classification.ts +52 -0
- package/build/migrations-v2/0000000000021-payment-line-classification-monitor-indexes.ts +31 -0
- package/build/migrations-v2/0000000000022-accounting-integrity-controlled-repairs.ts +281 -0
- package/build/migrations-v2/0000000000023-line-classification-usage.ts +325 -0
- package/build/migrations-v2/0000000000024-tenant-region-migration.ts +108 -0
- package/build/migrations-v2/0000000000025-partner-ach-denial-default.ts +30 -0
- package/build/migrations-v2/0000000000026-line-classification-usage-capture.ts +399 -0
- package/build/migrations-v2/0000000000027-payout-replacement-chain.ts +27 -0
- package/build/migrations-v2/0000000000028-accounting-integrity-operator-workflow.ts +108 -0
- package/build/migrations-v2/0000000000029-accounting-integrity-definition-artifacts.ts +40 -0
- package/build/migrations-v2/0000000000030-accounting-integrity-review-metadata-compatibility.ts +49 -0
- package/build/migrations-v2/0000000000031-ui-permission-role-simplification.ts +70 -0
- package/build/migrations-v2/0000000000032-tenant-delete-ownership-cascades.ts +44 -0
- package/build/migrations-v2/0000000000033-reservation-issue-seed-bucket-index.ts +19 -0
- package/build/migrations-v2/0000000000034-retire-recent-classification-indexes.ts +29 -0
- package/build/migrations-v2/0000000000035-core-change-autovacuum.ts +24 -0
- package/build/migrations-v2/0000000000036-journal-entry-inactive-reason-constraint.ts +33 -0
- package/build/migrations-v2/0000000000037-journal-entry-inactive-reasons.ts +19 -0
- package/build/migrations-v2/0000000000038-journal-entry-group-inactive-reason.ts +14 -0
- package/build/migrations-v2/0000000000039-reservation-guest-breakdown.ts +19 -0
- package/build/migrations-v2/0000000000040-csv-import-source-identity-index.ts +23 -0
- package/build/migrations-v2/0000000000041-payment-returns.ts +38 -0
- package/build/migrations-v2/0000000000042-operation-resource-types.ts +609 -0
- package/build/migrations-v2/0000000000043-reservation-journal-freshness.ts +149 -0
- package/build/migrations-v2/0000000000044-reservation-journal-freshness-backfill.ts +254 -0
- package/build/migrations-v2/0000000000045-reservation-journal-freshness-revision-repair.ts +83 -0
- package/build/migrations-v2/0000000000046-materialized-reservation-journal-operations.ts +709 -0
- package/build/migrations-v2/0000000000047-public-audit-completion.ts +77 -0
- package/build/migrations-v2/0000000000048-reservation-journal-full-refresh-repair.ts +136 -0
- package/build/migrations-v2/0000000000049-queued-journal-operation-failures.ts +60 -0
- package/build/migrations-v2/0000000000050-connection-oauth-state.ts +43 -0
- package/build/migrations-v2/0000000000051-owner-statement-email-delivery.ts +134 -0
- package/build/migrations-v2/0000000000052-fee-journal-lock-skip-operations.ts +59 -0
- package/build/migrations-v2/0000000000053-fee-journal-lock-skip-null-attempt.ts +44 -0
- package/build/migrations-v2/0000000000054-journal-refresh-issue-codes.ts +16 -0
- package/build/migrations-v2/0000000000055-tenant-billing-subscription.ts +8 -0
- package/build/module/control-plane/index.d.ts +312 -0
- package/build/module/control-plane/index.js.map +1 -1
- package/build/module/index.d.ts +13 -2
- package/build/module/index.js +219 -24
- package/build/module/index.js.map +1 -1
- package/build/module/local/generate.js +2 -0
- package/build/module/local/generate.js.map +1 -1
- package/build/module/local/index.js +110 -8
- package/build/module/local/index.js.map +1 -1
- package/build/module/postgres-error.d.ts +3 -3
- package/build/module/postgres-error.js +4 -3
- package/build/module/postgres-error.js.map +1 -1
- package/build/module/query/listing/status.js +2 -6
- package/build/module/query/listing/status.js.map +1 -1
- package/build/module/query/tenant/status.d.ts +1 -1
- package/build/module/query/tenant/status.js +6 -4
- package/build/module/query/tenant/status.js.map +1 -1
- package/build/module/regional.d.ts +1 -0
- package/build/module/regional.js +1 -0
- package/build/module/regional.js.map +1 -1
- package/build/module/test/index.d.ts +23 -3
- package/build/module/test/index.js +34 -13
- package/build/module/test/index.js.map +1 -1
- package/build/module/v1.generated.d.ts +560 -8
- package/build/module/v1.generated.js.map +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
import { sql } from 'kysely';
|
|
3
|
+
|
|
4
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
5
|
+
await sql`
|
|
6
|
+
create type audit.actor_organization_type as enum (
|
|
7
|
+
'vrplatform', 'partner', 'team', 'system'
|
|
8
|
+
)
|
|
9
|
+
`.execute(db);
|
|
10
|
+
await sql`
|
|
11
|
+
alter type audit.source_type add value if not exists 'embedded'
|
|
12
|
+
`.execute(db);
|
|
13
|
+
await sql`
|
|
14
|
+
alter type webhook.subscription_event_type
|
|
15
|
+
add value if not exists 'audit.event.changed'
|
|
16
|
+
`.execute(db);
|
|
17
|
+
await sql`
|
|
18
|
+
alter type webhook.event_type
|
|
19
|
+
add value if not exists 'audit.event.changed'
|
|
20
|
+
`.execute(db);
|
|
21
|
+
await sql`
|
|
22
|
+
alter type webhook.resource_type
|
|
23
|
+
add value if not exists 'auditEvent'
|
|
24
|
+
`.execute(db);
|
|
25
|
+
await sql`
|
|
26
|
+
alter table audit.action
|
|
27
|
+
add column actor_organization_type audit.actor_organization_type,
|
|
28
|
+
add column actor_organization_id uuid,
|
|
29
|
+
add column actor_organization_name text
|
|
30
|
+
`.execute(db);
|
|
31
|
+
await sql`
|
|
32
|
+
alter table audit.mutation
|
|
33
|
+
add column public_outcome jsonb
|
|
34
|
+
`.execute(db);
|
|
35
|
+
await sql`
|
|
36
|
+
alter table audit.public_event
|
|
37
|
+
add column actor_organization_type audit.actor_organization_type,
|
|
38
|
+
add column actor_organization_id uuid,
|
|
39
|
+
add column actor_organization_name text,
|
|
40
|
+
add column attempted_entity_changes jsonb not null default '[]'::jsonb,
|
|
41
|
+
add column blocked_by jsonb not null default '[]'::jsonb,
|
|
42
|
+
add column recovery jsonb not null default '[]'::jsonb
|
|
43
|
+
`.execute(db);
|
|
44
|
+
await sql`
|
|
45
|
+
create index if not exists public_event_tenant_occurred_idx
|
|
46
|
+
on audit.public_event (tenant_id, occurred_at, updated_at, id)
|
|
47
|
+
`.execute(db);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
51
|
+
await sql`
|
|
52
|
+
drop index if exists audit.public_event_tenant_occurred_idx
|
|
53
|
+
`.execute(db);
|
|
54
|
+
await sql`
|
|
55
|
+
alter table audit.public_event
|
|
56
|
+
drop column if exists recovery,
|
|
57
|
+
drop column if exists blocked_by,
|
|
58
|
+
drop column if exists attempted_entity_changes,
|
|
59
|
+
drop column if exists actor_organization_name,
|
|
60
|
+
drop column if exists actor_organization_id,
|
|
61
|
+
drop column if exists actor_organization_type
|
|
62
|
+
`.execute(db);
|
|
63
|
+
await sql`
|
|
64
|
+
alter table audit.action
|
|
65
|
+
drop column if exists actor_organization_name,
|
|
66
|
+
drop column if exists actor_organization_id,
|
|
67
|
+
drop column if exists actor_organization_type
|
|
68
|
+
`.execute(db);
|
|
69
|
+
await sql`
|
|
70
|
+
alter table audit.mutation
|
|
71
|
+
drop column if exists public_outcome
|
|
72
|
+
`.execute(db);
|
|
73
|
+
await sql`
|
|
74
|
+
drop type if exists audit.actor_organization_type
|
|
75
|
+
`.execute(db);
|
|
76
|
+
// PostgreSQL enum values cannot be removed safely without rebuilding source_type.
|
|
77
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
import { sql } from 'kysely';
|
|
3
|
+
|
|
4
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
5
|
+
await sql`
|
|
6
|
+
create temporary table reservation_journal_full_refresh_repair_scope (
|
|
7
|
+
invalidation_id uuid primary key,
|
|
8
|
+
tenant_id uuid not null,
|
|
9
|
+
reservation_id uuid not null,
|
|
10
|
+
applied_at timestamptz not null,
|
|
11
|
+
covering_effect_id uuid not null
|
|
12
|
+
) on commit drop
|
|
13
|
+
`.execute(db);
|
|
14
|
+
|
|
15
|
+
await sql`
|
|
16
|
+
insert into reservation_journal_full_refresh_repair_scope (
|
|
17
|
+
invalidation_id,
|
|
18
|
+
tenant_id,
|
|
19
|
+
reservation_id,
|
|
20
|
+
applied_at,
|
|
21
|
+
covering_effect_id
|
|
22
|
+
)
|
|
23
|
+
select
|
|
24
|
+
pending.id,
|
|
25
|
+
pending.tenant_id,
|
|
26
|
+
pending.reservation_id,
|
|
27
|
+
covering.applied_at,
|
|
28
|
+
covering.effect_id
|
|
29
|
+
from accounting.reservation_journal_invalidation as pending
|
|
30
|
+
inner join lateral (
|
|
31
|
+
select
|
|
32
|
+
applied.applied_at,
|
|
33
|
+
applied.effect_id
|
|
34
|
+
from accounting.reservation_journal_invalidation as applied
|
|
35
|
+
inner join audit.effect as effect
|
|
36
|
+
on effect.tenant_id = applied.tenant_id
|
|
37
|
+
and effect.id = applied.effect_id
|
|
38
|
+
where applied.tenant_id = pending.tenant_id
|
|
39
|
+
and applied.reservation_id = pending.reservation_id
|
|
40
|
+
and applied.revision >= pending.revision
|
|
41
|
+
and applied.applied_at is not null
|
|
42
|
+
and applied.covered_by_effect_id = applied.effect_id
|
|
43
|
+
and effect.status = 'completed'
|
|
44
|
+
and coalesce(effect.payload ->> 'mode', 'full') = 'full'
|
|
45
|
+
and (
|
|
46
|
+
select attempt.status
|
|
47
|
+
from audit.effect_attempt as attempt
|
|
48
|
+
where attempt.tenant_id = effect.tenant_id
|
|
49
|
+
and attempt.effect_id = effect.id
|
|
50
|
+
order by attempt.attempt_num desc
|
|
51
|
+
limit 1
|
|
52
|
+
) is distinct from 'skipped_locked'
|
|
53
|
+
order by applied.revision desc
|
|
54
|
+
limit 1
|
|
55
|
+
) as covering on true
|
|
56
|
+
where pending.applied_at is null
|
|
57
|
+
`.execute(db);
|
|
58
|
+
|
|
59
|
+
await sql`
|
|
60
|
+
update accounting.reservation_journal_invalidation as invalidation
|
|
61
|
+
set
|
|
62
|
+
applied_at = scope.applied_at,
|
|
63
|
+
covered_by_effect_id = scope.covering_effect_id
|
|
64
|
+
from reservation_journal_full_refresh_repair_scope as scope
|
|
65
|
+
where invalidation.id = scope.invalidation_id
|
|
66
|
+
and invalidation.applied_at is null
|
|
67
|
+
`.execute(db);
|
|
68
|
+
|
|
69
|
+
await sql`
|
|
70
|
+
select state.tenant_id
|
|
71
|
+
from accounting.reservation_journal_state as state
|
|
72
|
+
inner join (
|
|
73
|
+
select distinct tenant_id, reservation_id
|
|
74
|
+
from reservation_journal_full_refresh_repair_scope
|
|
75
|
+
) as scope
|
|
76
|
+
on scope.tenant_id = state.tenant_id
|
|
77
|
+
and scope.reservation_id = state.reservation_id
|
|
78
|
+
order by state.tenant_id, state.reservation_id
|
|
79
|
+
for update of state
|
|
80
|
+
`.execute(db);
|
|
81
|
+
|
|
82
|
+
await sql`
|
|
83
|
+
with advancement as (
|
|
84
|
+
select
|
|
85
|
+
state.tenant_id,
|
|
86
|
+
state.reservation_id,
|
|
87
|
+
coalesce(
|
|
88
|
+
min(invalidation.revision) filter (
|
|
89
|
+
where invalidation.applied_at is null
|
|
90
|
+
) - 1,
|
|
91
|
+
max(invalidation.revision),
|
|
92
|
+
state.applied_revision
|
|
93
|
+
) as applied_revision,
|
|
94
|
+
min(invalidation.created_at) filter (
|
|
95
|
+
where invalidation.applied_at is null
|
|
96
|
+
) as stale_since,
|
|
97
|
+
max(invalidation.applied_at) as calculated_at
|
|
98
|
+
from accounting.reservation_journal_state as state
|
|
99
|
+
inner join (
|
|
100
|
+
select distinct tenant_id, reservation_id
|
|
101
|
+
from reservation_journal_full_refresh_repair_scope
|
|
102
|
+
) as scope
|
|
103
|
+
on scope.tenant_id = state.tenant_id
|
|
104
|
+
and scope.reservation_id = state.reservation_id
|
|
105
|
+
left join accounting.reservation_journal_invalidation as invalidation
|
|
106
|
+
on invalidation.tenant_id = state.tenant_id
|
|
107
|
+
and invalidation.reservation_id = state.reservation_id
|
|
108
|
+
group by
|
|
109
|
+
state.tenant_id,
|
|
110
|
+
state.reservation_id,
|
|
111
|
+
state.applied_revision
|
|
112
|
+
)
|
|
113
|
+
update accounting.reservation_journal_state as state
|
|
114
|
+
set
|
|
115
|
+
applied_revision = greatest(
|
|
116
|
+
state.applied_revision,
|
|
117
|
+
advancement.applied_revision
|
|
118
|
+
),
|
|
119
|
+
stale_since = advancement.stale_since,
|
|
120
|
+
calculated_at = case
|
|
121
|
+
when advancement.applied_revision > state.applied_revision
|
|
122
|
+
then greatest(state.calculated_at, advancement.calculated_at)
|
|
123
|
+
else state.calculated_at
|
|
124
|
+
end,
|
|
125
|
+
updated_at = now()
|
|
126
|
+
from advancement
|
|
127
|
+
where state.tenant_id = advancement.tenant_id
|
|
128
|
+
and state.reservation_id = advancement.reservation_id
|
|
129
|
+
and (
|
|
130
|
+
state.applied_revision < advancement.applied_revision
|
|
131
|
+
or state.stale_since is distinct from advancement.stale_since
|
|
132
|
+
)
|
|
133
|
+
`.execute(db);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function down(_db: Kysely<unknown>): Promise<void> {}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type Kysely, sql } from 'kysely';
|
|
2
|
+
|
|
3
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
4
|
+
// Preserve the source lifecycle and failure precedence; ID format is not a failure policy.
|
|
5
|
+
await sql`
|
|
6
|
+
do $$
|
|
7
|
+
declare
|
|
8
|
+
definition text;
|
|
9
|
+
begin
|
|
10
|
+
definition := pg_get_functiondef('core.refresh_operation_state(uuid)'::regprocedure);
|
|
11
|
+
if position('and target_public_id_format = ''action''' in definition) = 0 then
|
|
12
|
+
raise exception 'Journal operation failure projection has an unexpected definition';
|
|
13
|
+
end if;
|
|
14
|
+
execute replace(definition, 'and target_public_id_format = ''action''', '');
|
|
15
|
+
end;
|
|
16
|
+
$$
|
|
17
|
+
`.execute(db);
|
|
18
|
+
|
|
19
|
+
await sql`
|
|
20
|
+
select core.refresh_operation_state(operation.id)
|
|
21
|
+
from core.operation as operation
|
|
22
|
+
where operation.type = 'reservation-journal-refresh'
|
|
23
|
+
and operation.public_id_format = 'legacy'
|
|
24
|
+
and operation.status = 'failed'
|
|
25
|
+
and exists (
|
|
26
|
+
select 1 from core.operation_source as source
|
|
27
|
+
where source.operation_id = operation.id
|
|
28
|
+
and source.source_type = 'audit-action'
|
|
29
|
+
)
|
|
30
|
+
`.execute(db);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
34
|
+
await sql`
|
|
35
|
+
do $$
|
|
36
|
+
declare
|
|
37
|
+
definition text;
|
|
38
|
+
begin
|
|
39
|
+
definition := pg_get_functiondef('core.refresh_operation_state(uuid)'::regprocedure);
|
|
40
|
+
if position('and target_public_id_format = ''action''' in definition) > 0
|
|
41
|
+
or position('and target_action_id is not null then' in definition) = 0 then
|
|
42
|
+
raise exception 'Journal operation failure projection has an unexpected definition';
|
|
43
|
+
end if;
|
|
44
|
+
execute replace(
|
|
45
|
+
definition,
|
|
46
|
+
'and target_action_id is not null then',
|
|
47
|
+
'and target_public_id_format = ''action'' and target_action_id is not null then'
|
|
48
|
+
);
|
|
49
|
+
end;
|
|
50
|
+
$$
|
|
51
|
+
`.execute(db);
|
|
52
|
+
|
|
53
|
+
await sql`
|
|
54
|
+
select core.refresh_operation_state(operation.id)
|
|
55
|
+
from core.operation as operation
|
|
56
|
+
where operation.type = 'reservation-journal-refresh'
|
|
57
|
+
and operation.public_id_format = 'legacy'
|
|
58
|
+
and operation.status = 'failed'
|
|
59
|
+
`.execute(db);
|
|
60
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
import { sql } from 'kysely';
|
|
3
|
+
|
|
4
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
5
|
+
await sql`set local lock_timeout = '5s'`.execute(db);
|
|
6
|
+
await sql`set local statement_timeout = '60s'`.execute(db);
|
|
7
|
+
await sql`
|
|
8
|
+
create table public.connection_oauth_state (
|
|
9
|
+
state_hash text primary key,
|
|
10
|
+
tenant_id uuid not null references public.tenant (id) on delete cascade,
|
|
11
|
+
user_id uuid not null,
|
|
12
|
+
app_id text not null,
|
|
13
|
+
connection_id uuid,
|
|
14
|
+
params jsonb not null,
|
|
15
|
+
redirect_uri text not null,
|
|
16
|
+
expires_at timestamptz not null,
|
|
17
|
+
constraint connection_oauth_state_hash_shape check (state_hash ~ '^[0-9a-f]{64}$'),
|
|
18
|
+
constraint connection_oauth_state_params_object check (jsonb_typeof(params) = 'object')
|
|
19
|
+
)
|
|
20
|
+
`.execute(db);
|
|
21
|
+
await sql`create index connection_oauth_state_expiry_idx on public.connection_oauth_state (expires_at)`.execute(
|
|
22
|
+
db
|
|
23
|
+
);
|
|
24
|
+
await sql`create index connection_oauth_state_tenant_idx on public.connection_oauth_state (tenant_id)`.execute(
|
|
25
|
+
db
|
|
26
|
+
);
|
|
27
|
+
await sql`
|
|
28
|
+
do $$
|
|
29
|
+
begin
|
|
30
|
+
if exists (select 1 from pg_roles where rolname = 'application') then
|
|
31
|
+
grant select, insert, delete on public.connection_oauth_state to application;
|
|
32
|
+
-- SELECT FOR UPDATE SKIP LOCKED needs UPDATE on at least one column.
|
|
33
|
+
-- State bindings, parameters, and expiry remain non-updatable at runtime.
|
|
34
|
+
grant update (state_hash) on public.connection_oauth_state to application;
|
|
35
|
+
end if;
|
|
36
|
+
end;
|
|
37
|
+
$$
|
|
38
|
+
`.execute(db);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
42
|
+
await sql`drop table public.connection_oauth_state`.execute(db);
|
|
43
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
import { sql } from 'kysely';
|
|
3
|
+
|
|
4
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
5
|
+
await sql`set local lock_timeout = '5s'`.execute(db);
|
|
6
|
+
await sql`set local statement_timeout = '60s'`.execute(db);
|
|
7
|
+
|
|
8
|
+
await sql`
|
|
9
|
+
create type core.email_notification_status as enum (
|
|
10
|
+
'queued', 'processing', 'completed', 'failed', 'canceled'
|
|
11
|
+
)
|
|
12
|
+
`.execute(db);
|
|
13
|
+
await sql`
|
|
14
|
+
create type core.email_delivery_status as enum (
|
|
15
|
+
'queued', 'sending', 'retrying', 'accepted', 'rejected', 'failed',
|
|
16
|
+
'canceled'
|
|
17
|
+
)
|
|
18
|
+
`.execute(db);
|
|
19
|
+
await sql`
|
|
20
|
+
create type core.email_delivery_failure_code as enum (
|
|
21
|
+
'inactive_recipient',
|
|
22
|
+
'invalid_provider_response',
|
|
23
|
+
'postmark_not_configured',
|
|
24
|
+
'provider_rejected',
|
|
25
|
+
'task_failed',
|
|
26
|
+
'transport_failed'
|
|
27
|
+
)
|
|
28
|
+
`.execute(db);
|
|
29
|
+
|
|
30
|
+
await sql`
|
|
31
|
+
create table core.email_notification (
|
|
32
|
+
id uuid primary key references audit.effect(id) on delete cascade,
|
|
33
|
+
tenant_id uuid not null references public.tenant(id) on delete cascade,
|
|
34
|
+
owner_statement_id uuid not null,
|
|
35
|
+
status core.email_notification_status not null default 'queued',
|
|
36
|
+
trigger_run_id text,
|
|
37
|
+
recipients_resolved_at timestamptz,
|
|
38
|
+
started_at timestamptz,
|
|
39
|
+
finished_at timestamptz,
|
|
40
|
+
created_at timestamptz not null default now(),
|
|
41
|
+
updated_at timestamptz not null default now(),
|
|
42
|
+
constraint email_notification_terminal_check check (
|
|
43
|
+
(status in ('completed', 'failed', 'canceled') and finished_at is not null)
|
|
44
|
+
or (status in ('queued', 'processing') and finished_at is null)
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
`.execute(db);
|
|
48
|
+
await sql`
|
|
49
|
+
create index email_notification_statement_history_idx
|
|
50
|
+
on core.email_notification (
|
|
51
|
+
tenant_id, owner_statement_id, created_at desc, id desc
|
|
52
|
+
)
|
|
53
|
+
`.execute(db);
|
|
54
|
+
|
|
55
|
+
await sql`
|
|
56
|
+
create table core.email_delivery (
|
|
57
|
+
id uuid primary key default gen_random_uuid(),
|
|
58
|
+
notification_id uuid not null
|
|
59
|
+
references core.email_notification(id) on delete cascade,
|
|
60
|
+
tenant_id uuid not null references public.tenant(id) on delete cascade,
|
|
61
|
+
user_id uuid not null,
|
|
62
|
+
status core.email_delivery_status not null default 'queued',
|
|
63
|
+
attempt_count integer not null default 0,
|
|
64
|
+
trigger_run_id text,
|
|
65
|
+
postmark_message_id text,
|
|
66
|
+
postmark_error_code integer,
|
|
67
|
+
http_status integer,
|
|
68
|
+
failure_code core.email_delivery_failure_code,
|
|
69
|
+
submitted_at timestamptz,
|
|
70
|
+
last_attempt_at timestamptz,
|
|
71
|
+
finished_at timestamptz,
|
|
72
|
+
created_at timestamptz not null default now(),
|
|
73
|
+
updated_at timestamptz not null default now(),
|
|
74
|
+
constraint email_delivery_notification_user_key
|
|
75
|
+
unique (notification_id, user_id),
|
|
76
|
+
constraint email_delivery_attempt_count_check check (attempt_count >= 0),
|
|
77
|
+
constraint email_delivery_postmark_error_code_check check (
|
|
78
|
+
postmark_error_code is null or postmark_error_code >= 0
|
|
79
|
+
),
|
|
80
|
+
constraint email_delivery_http_status_check check (
|
|
81
|
+
http_status is null or http_status between 100 and 599
|
|
82
|
+
),
|
|
83
|
+
constraint email_delivery_terminal_check check (
|
|
84
|
+
(status in ('accepted', 'rejected', 'failed', 'canceled')
|
|
85
|
+
and finished_at is not null)
|
|
86
|
+
or (status in ('queued', 'sending', 'retrying') and finished_at is null)
|
|
87
|
+
),
|
|
88
|
+
constraint email_delivery_accepted_check check (
|
|
89
|
+
status != 'accepted'
|
|
90
|
+
or (
|
|
91
|
+
postmark_message_id is not null
|
|
92
|
+
and submitted_at is not null
|
|
93
|
+
and failure_code is null
|
|
94
|
+
)
|
|
95
|
+
),
|
|
96
|
+
constraint email_delivery_failure_check check (
|
|
97
|
+
status not in ('rejected', 'failed') or failure_code is not null
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
`.execute(db);
|
|
101
|
+
await sql`
|
|
102
|
+
create index email_delivery_notification_status_idx
|
|
103
|
+
on core.email_delivery (notification_id, status, id)
|
|
104
|
+
`.execute(db);
|
|
105
|
+
await sql`
|
|
106
|
+
create index email_delivery_tenant_created_idx
|
|
107
|
+
on core.email_delivery (tenant_id, created_at desc, id desc)
|
|
108
|
+
`.execute(db);
|
|
109
|
+
await sql`
|
|
110
|
+
create unique index email_delivery_postmark_message_idx
|
|
111
|
+
on core.email_delivery (postmark_message_id)
|
|
112
|
+
where postmark_message_id is not null
|
|
113
|
+
`.execute(db);
|
|
114
|
+
|
|
115
|
+
await sql`
|
|
116
|
+
do $$
|
|
117
|
+
begin
|
|
118
|
+
if exists (select 1 from pg_roles where rolname = 'application') then
|
|
119
|
+
grant select, insert, update
|
|
120
|
+
on core.email_notification, core.email_delivery
|
|
121
|
+
to application;
|
|
122
|
+
end if;
|
|
123
|
+
end;
|
|
124
|
+
$$
|
|
125
|
+
`.execute(db);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
129
|
+
await sql`drop table core.email_delivery`.execute(db);
|
|
130
|
+
await sql`drop table core.email_notification`.execute(db);
|
|
131
|
+
await sql`drop type core.email_delivery_failure_code`.execute(db);
|
|
132
|
+
await sql`drop type core.email_delivery_status`.execute(db);
|
|
133
|
+
await sql`drop type core.email_notification_status`.execute(db);
|
|
134
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Kysely, sql } from 'kysely';
|
|
2
|
+
|
|
3
|
+
const original = `invalidation.id is null
|
|
4
|
+
and effect.status = 'completed'`;
|
|
5
|
+
const corrected = `(invalidation.id is null or (
|
|
6
|
+
exists (
|
|
7
|
+
select 1 from audit.action as action
|
|
8
|
+
where action.id = effect.action_id
|
|
9
|
+
and action.tenant_id = effect.tenant_id
|
|
10
|
+
and action.root_entity_type = 'recurringFee'
|
|
11
|
+
)
|
|
12
|
+
and (
|
|
13
|
+
select attempt.status from audit.effect_attempt as attempt
|
|
14
|
+
where attempt.effect_id = effect.id
|
|
15
|
+
and attempt.tenant_id = effect.tenant_id
|
|
16
|
+
order by attempt.attempt_num desc limit 1
|
|
17
|
+
) = 'skipped_locked'
|
|
18
|
+
))
|
|
19
|
+
and effect.status = 'completed'`;
|
|
20
|
+
|
|
21
|
+
async function replaceFeeLockSkipProjection<Database>(
|
|
22
|
+
db: Kysely<Database>,
|
|
23
|
+
before: string,
|
|
24
|
+
after: string
|
|
25
|
+
) {
|
|
26
|
+
const { rows } = await sql<{ definition: string }>`
|
|
27
|
+
select pg_get_functiondef('core.refresh_audit_action_source(uuid)'::regprocedure)
|
|
28
|
+
as definition
|
|
29
|
+
`.execute(db);
|
|
30
|
+
const definition = rows[0]?.definition;
|
|
31
|
+
if (!definition || definition.split(before).length !== 2) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'Journal operation source projection has an unexpected definition'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
await sql.raw(definition.replace(before, after)).execute(db);
|
|
37
|
+
// Reproject derived operation state only. Preserve original attempts and journal invalidations.
|
|
38
|
+
await sql`
|
|
39
|
+
select core.refresh_audit_action_source(action.id)
|
|
40
|
+
from audit.action as action
|
|
41
|
+
where action.root_entity_type = 'recurringFee'
|
|
42
|
+
and exists (
|
|
43
|
+
select 1 from core.operation_source as source
|
|
44
|
+
inner join core.operation as operation on operation.id = source.operation_id
|
|
45
|
+
where source.source_type = 'audit-action'
|
|
46
|
+
and source.source_id = action.id
|
|
47
|
+
and operation.tenant_id = action.tenant_id
|
|
48
|
+
and operation.type = 'reservation-journal-refresh'
|
|
49
|
+
)
|
|
50
|
+
`.execute(db);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function up<Database>(db: Kysely<Database>): Promise<void> {
|
|
54
|
+
await replaceFeeLockSkipProjection(db, original, corrected);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function down<Database>(db: Kysely<Database>): Promise<void> {
|
|
58
|
+
await replaceFeeLockSkipProjection(db, corrected, original);
|
|
59
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Kysely, sql } from 'kysely';
|
|
2
|
+
|
|
3
|
+
async function replaceNullableLockSkip<Database>(
|
|
4
|
+
db: Kysely<Database>,
|
|
5
|
+
before: string,
|
|
6
|
+
after: string
|
|
7
|
+
) {
|
|
8
|
+
const { rows } = await sql<{ definition: string }>`
|
|
9
|
+
select pg_get_functiondef('core.refresh_audit_action_source(uuid)'::regprocedure) as definition
|
|
10
|
+
`.execute(db);
|
|
11
|
+
const definition = rows[0]?.definition;
|
|
12
|
+
if (!definition || definition.split(before).length !== 2) {
|
|
13
|
+
throw new Error('Fee lock skip projection has an unexpected definition');
|
|
14
|
+
}
|
|
15
|
+
await sql.raw(definition.replace(before, after)).execute(db);
|
|
16
|
+
await sql`
|
|
17
|
+
select core.refresh_audit_action_source(action.id)
|
|
18
|
+
from audit.action as action
|
|
19
|
+
where action.root_entity_type = 'recurringFee'
|
|
20
|
+
and exists (
|
|
21
|
+
select 1 from core.operation_source as source
|
|
22
|
+
inner join core.operation as operation on operation.id = source.operation_id
|
|
23
|
+
where source.source_type = 'audit-action' and source.source_id = action.id
|
|
24
|
+
and operation.tenant_id = action.tenant_id
|
|
25
|
+
and operation.type = 'reservation-journal-refresh'
|
|
26
|
+
)
|
|
27
|
+
`.execute(db);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function up<Database>(db: Kysely<Database>): Promise<void> {
|
|
31
|
+
await replaceNullableLockSkip(
|
|
32
|
+
db,
|
|
33
|
+
") = 'skipped_locked'",
|
|
34
|
+
") is not distinct from 'skipped_locked'"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function down<Database>(db: Kysely<Database>): Promise<void> {
|
|
39
|
+
await replaceNullableLockSkip(
|
|
40
|
+
db,
|
|
41
|
+
") is not distinct from 'skipped_locked'",
|
|
42
|
+
") = 'skipped_locked'"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
import { sql } from 'kysely';
|
|
3
|
+
|
|
4
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
5
|
+
// Expand storage only. New codes must be readable on every channel before
|
|
6
|
+
// issue collectors begin writing them to shared regional databases.
|
|
7
|
+
await sql`alter type health.tenant_issue_code add value if not exists 'journalLocked'`.execute(
|
|
8
|
+
db
|
|
9
|
+
);
|
|
10
|
+
await sql`alter type health.tenant_issue_code add value if not exists 'journalConfigurationInvalid'`.execute(
|
|
11
|
+
db
|
|
12
|
+
);
|
|
13
|
+
await sql`alter type health.tenant_issue_code add value if not exists 'journalUnbalanced'`.execute(
|
|
14
|
+
db
|
|
15
|
+
);
|
|
16
|
+
}
|