feat: establish standalone Device Core repository
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
begin;
|
||||
|
||||
create table if not exists device_model_profiles (
|
||||
profile_ref text primary key,
|
||||
schema_version text not null,
|
||||
vendor text not null,
|
||||
model text not null,
|
||||
device_type text not null,
|
||||
protocol text not null,
|
||||
profile jsonb not null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists device_contours (
|
||||
id uuid primary key,
|
||||
owner_scope text not null,
|
||||
name text not null,
|
||||
lifecycle_state text not null default 'active'
|
||||
check (lifecycle_state in ('active', 'suspended', 'retired')),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (owner_scope, name)
|
||||
);
|
||||
|
||||
create table if not exists device_discoveries (
|
||||
id uuid primary key,
|
||||
identifier_kind text not null,
|
||||
identifier_digest text not null,
|
||||
identifier_masked text not null,
|
||||
model_profile_ref text not null references device_model_profiles(profile_ref),
|
||||
protocol text not null,
|
||||
lifecycle_state text not null default 'quarantine'
|
||||
check (lifecycle_state in ('quarantine', 'claimed', 'rejected', 'expired')),
|
||||
first_observed_at timestamptz not null,
|
||||
last_observed_at timestamptz not null,
|
||||
evidence jsonb not null,
|
||||
claimed_device_id uuid,
|
||||
claimed_at timestamptz,
|
||||
claimed_by text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (identifier_kind, identifier_digest, model_profile_ref)
|
||||
);
|
||||
|
||||
create index if not exists device_discoveries_state_last_seen_idx
|
||||
on device_discoveries (lifecycle_state, last_observed_at desc);
|
||||
|
||||
create table if not exists device_instances (
|
||||
id uuid primary key,
|
||||
contour_id uuid not null references device_contours(id),
|
||||
model_profile_ref text not null references device_model_profiles(profile_ref),
|
||||
display_name text not null,
|
||||
identifier_kind text not null,
|
||||
identifier_digest text not null,
|
||||
identifier_masked text not null,
|
||||
credential_ref text,
|
||||
lifecycle_state text not null default 'claimed'
|
||||
check (lifecycle_state in ('claimed', 'online', 'offline', 'suspended', 'retired')),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (identifier_kind, identifier_digest, model_profile_ref)
|
||||
);
|
||||
|
||||
alter table device_discoveries
|
||||
drop constraint if exists device_discoveries_claimed_device_fk;
|
||||
|
||||
alter table device_discoveries
|
||||
add constraint device_discoveries_claimed_device_fk
|
||||
foreign key (claimed_device_id) references device_instances(id);
|
||||
|
||||
create table if not exists device_bindings (
|
||||
id uuid primary key,
|
||||
contour_id uuid not null references device_contours(id),
|
||||
target_kind text not null,
|
||||
target_ref text not null,
|
||||
capabilities text[] not null,
|
||||
lifecycle_state text not null default 'active'
|
||||
check (lifecycle_state in ('active', 'revoked')),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (contour_id, target_kind, target_ref)
|
||||
);
|
||||
|
||||
create table if not exists device_audit_events (
|
||||
id uuid primary key,
|
||||
event_type text not null,
|
||||
actor_ref text not null,
|
||||
contour_id uuid,
|
||||
device_id uuid,
|
||||
discovery_id uuid,
|
||||
payload jsonb not null,
|
||||
occurred_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists device_audit_events_device_time_idx
|
||||
on device_audit_events (device_id, occurred_at desc);
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,113 @@
|
||||
begin;
|
||||
|
||||
create table if not exists device_owner_scopes (
|
||||
id uuid primary key,
|
||||
scope_kind text not null
|
||||
check (scope_kind in ('company', 'personal')),
|
||||
owner_ref text not null
|
||||
check (length(btrim(owner_ref)) between 3 and 256),
|
||||
display_name text not null
|
||||
check (length(btrim(display_name)) between 1 and 160),
|
||||
lifecycle_state text not null default 'active'
|
||||
check (lifecycle_state in ('active', 'suspended', 'retired')),
|
||||
created_by_ref text not null
|
||||
check (length(btrim(created_by_ref)) between 3 and 256),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (scope_kind, owner_ref)
|
||||
);
|
||||
|
||||
create table if not exists device_projects (
|
||||
id uuid primary key,
|
||||
owner_scope_id uuid not null references device_owner_scopes(id),
|
||||
project_key text not null
|
||||
check (project_key ~ '^[a-z][a-z0-9-]{1,62}$'),
|
||||
name text not null
|
||||
check (length(btrim(name)) between 1 and 160),
|
||||
description text,
|
||||
lifecycle_state text not null default 'active'
|
||||
check (lifecycle_state in ('active', 'suspended', 'archived')),
|
||||
created_by_ref text not null
|
||||
check (length(btrim(created_by_ref)) between 3 and 256),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (owner_scope_id, project_key)
|
||||
);
|
||||
|
||||
create index if not exists device_projects_owner_scope_idx
|
||||
on device_projects (owner_scope_id, lifecycle_state, updated_at desc);
|
||||
|
||||
alter table device_instances
|
||||
add column if not exists project_id uuid references device_projects(id);
|
||||
|
||||
create unique index if not exists device_instances_id_project_idx
|
||||
on device_instances (id, project_id);
|
||||
|
||||
create table if not exists device_collections (
|
||||
id uuid primary key,
|
||||
project_id uuid not null references device_projects(id),
|
||||
collection_key text not null
|
||||
check (collection_key ~ '^[a-z][a-z0-9-]{1,62}$'),
|
||||
name text not null
|
||||
check (length(btrim(name)) between 1 and 160),
|
||||
description text,
|
||||
lifecycle_state text not null default 'active'
|
||||
check (lifecycle_state in ('active', 'archived')),
|
||||
created_by_ref text not null
|
||||
check (length(btrim(created_by_ref)) between 3 and 256),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (project_id, collection_key),
|
||||
unique (id, project_id)
|
||||
);
|
||||
|
||||
create index if not exists device_collections_project_idx
|
||||
on device_collections (project_id, lifecycle_state, updated_at desc);
|
||||
|
||||
create table if not exists device_collection_members (
|
||||
collection_id uuid not null,
|
||||
device_id uuid not null,
|
||||
project_id uuid not null references device_projects(id),
|
||||
added_by_ref text not null
|
||||
check (length(btrim(added_by_ref)) between 3 and 256),
|
||||
added_at timestamptz not null default now(),
|
||||
primary key (collection_id, device_id),
|
||||
foreign key (collection_id, project_id)
|
||||
references device_collections(id, project_id),
|
||||
foreign key (device_id, project_id)
|
||||
references device_instances(id, project_id)
|
||||
);
|
||||
|
||||
create index if not exists device_collection_members_device_idx
|
||||
on device_collection_members (device_id, collection_id);
|
||||
|
||||
create table if not exists device_project_grants (
|
||||
id uuid primary key,
|
||||
project_id uuid not null references device_projects(id),
|
||||
principal_kind text not null
|
||||
check (principal_kind in ('user', 'group')),
|
||||
principal_ref text not null
|
||||
check (length(btrim(principal_ref)) between 3 and 256),
|
||||
project_role text not null
|
||||
check (project_role in ('viewer', 'operator', 'engineer', 'admin', 'owner')),
|
||||
capability_allow text[] not null default '{}',
|
||||
capability_deny text[] not null default '{}',
|
||||
lifecycle_state text not null default 'active'
|
||||
check (lifecycle_state in ('active', 'revoked')),
|
||||
created_by_ref text not null
|
||||
check (length(btrim(created_by_ref)) between 3 and 256),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (project_id, principal_kind, principal_ref),
|
||||
check (not (capability_allow && capability_deny))
|
||||
);
|
||||
|
||||
create index if not exists device_project_grants_principal_idx
|
||||
on device_project_grants (
|
||||
principal_kind,
|
||||
principal_ref,
|
||||
lifecycle_state,
|
||||
project_id
|
||||
);
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,68 @@
|
||||
begin;
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1
|
||||
from pg_constraint
|
||||
where conname = 'device_project_grants_owner_user_only'
|
||||
and conrelid = 'device_project_grants'::regclass
|
||||
) then
|
||||
alter table device_project_grants
|
||||
add constraint device_project_grants_owner_user_only
|
||||
check (project_role <> 'owner' or principal_kind = 'user');
|
||||
end if;
|
||||
end
|
||||
$$;
|
||||
|
||||
alter table device_audit_events
|
||||
add column if not exists project_id uuid references device_projects(id);
|
||||
|
||||
create index if not exists device_audit_events_project_time_idx
|
||||
on device_audit_events (project_id, occurred_at desc);
|
||||
|
||||
create table if not exists device_management_command_receipts (
|
||||
id uuid primary key,
|
||||
actor_ref text not null
|
||||
check (length(btrim(actor_ref)) between 3 and 256),
|
||||
command_kind text not null
|
||||
check (command_kind in (
|
||||
'owner_scope.ensure',
|
||||
'project.ensure',
|
||||
'collection.ensure',
|
||||
'project_grant.upsert'
|
||||
)),
|
||||
idempotency_key text not null
|
||||
check (length(idempotency_key) between 8 and 256),
|
||||
request_digest text not null
|
||||
check (request_digest ~ '^sha256:[a-f0-9]{64}$'),
|
||||
lifecycle_state text not null default 'pending'
|
||||
check (lifecycle_state in ('pending', 'completed')),
|
||||
response_status integer
|
||||
check (response_status between 200 and 599),
|
||||
response_body jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
completed_at timestamptz,
|
||||
unique (actor_ref, command_kind, idempotency_key),
|
||||
check (
|
||||
(
|
||||
lifecycle_state = 'pending'
|
||||
and response_status is null
|
||||
and response_body is null
|
||||
and completed_at is null
|
||||
)
|
||||
or
|
||||
(
|
||||
lifecycle_state = 'completed'
|
||||
and response_status is not null
|
||||
and response_body is not null
|
||||
and completed_at is not null
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
create index if not exists device_management_receipts_created_idx
|
||||
on device_management_command_receipts (created_at desc);
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,208 @@
|
||||
begin;
|
||||
|
||||
create table if not exists device_adapter_packages (
|
||||
id uuid primary key,
|
||||
package_key text not null
|
||||
check (package_key ~ '^[a-z][a-z0-9-]{1,62}$'),
|
||||
display_name text not null
|
||||
check (length(btrim(display_name)) between 1 and 160),
|
||||
publisher_ref text not null
|
||||
check (length(btrim(publisher_ref)) between 3 and 256),
|
||||
lifecycle_state text not null default 'active'
|
||||
check (lifecycle_state in ('active', 'retired')),
|
||||
created_by_ref text not null
|
||||
check (length(btrim(created_by_ref)) between 3 and 256),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (package_key)
|
||||
);
|
||||
|
||||
create table if not exists device_adapter_versions (
|
||||
id uuid primary key,
|
||||
adapter_package_id uuid not null references device_adapter_packages(id),
|
||||
version text not null
|
||||
check (version ~ '^[0-9]+\.[0-9]+\.[0-9]+([+-][A-Za-z0-9.-]+)?$'),
|
||||
runtime_package_ref text not null
|
||||
check (length(btrim(runtime_package_ref)) between 3 and 256),
|
||||
content_digest text not null
|
||||
check (content_digest ~ '^sha256:[a-f0-9]{64}$'),
|
||||
contract_version text not null
|
||||
check (contract_version ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$'),
|
||||
capabilities text[] not null default '{}'
|
||||
check (
|
||||
cardinality(capabilities) <= 64
|
||||
and array_position(capabilities, null) is null
|
||||
),
|
||||
lifecycle_state text not null default 'draft'
|
||||
check (lifecycle_state in ('draft', 'active', 'retired')),
|
||||
registered_by_ref text not null
|
||||
check (length(btrim(registered_by_ref)) between 3 and 256),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (adapter_package_id, version),
|
||||
unique (runtime_package_ref, content_digest),
|
||||
unique (id, adapter_package_id)
|
||||
);
|
||||
|
||||
alter table device_model_profiles
|
||||
add column if not exists adapter_version_id uuid
|
||||
references device_adapter_versions(id),
|
||||
add column if not exists schema_artifact_ref text
|
||||
check (
|
||||
schema_artifact_ref is null
|
||||
or length(btrim(schema_artifact_ref)) between 3 and 256
|
||||
),
|
||||
add column if not exists profile_digest text
|
||||
check (
|
||||
profile_digest is null
|
||||
or profile_digest ~ '^sha256:[a-f0-9]{64}$'
|
||||
),
|
||||
add column if not exists capabilities text[] not null default '{}'
|
||||
check (
|
||||
cardinality(capabilities) <= 64
|
||||
and array_position(capabilities, null) is null
|
||||
),
|
||||
add column if not exists lifecycle_state text not null default 'active'
|
||||
check (lifecycle_state in ('draft', 'active', 'retired'));
|
||||
|
||||
create index if not exists device_model_profiles_adapter_version_idx
|
||||
on device_model_profiles (adapter_version_id, lifecycle_state, updated_at desc);
|
||||
|
||||
create table if not exists device_edges (
|
||||
id uuid primary key,
|
||||
edge_key text not null
|
||||
check (edge_key ~ '^[a-z][a-z0-9-]{1,62}$'),
|
||||
display_name text not null
|
||||
check (length(btrim(display_name)) between 1 and 160),
|
||||
deployment_ref text
|
||||
check (
|
||||
deployment_ref is null
|
||||
or length(btrim(deployment_ref)) between 3 and 256
|
||||
),
|
||||
lifecycle_state text not null default 'provisioning'
|
||||
check (lifecycle_state in ('provisioning', 'active', 'suspended', 'retired')),
|
||||
created_by_ref text not null
|
||||
check (length(btrim(created_by_ref)) between 3 and 256),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (edge_key)
|
||||
);
|
||||
|
||||
create table if not exists device_routes (
|
||||
id uuid primary key,
|
||||
project_id uuid not null references device_projects(id),
|
||||
route_key text not null
|
||||
check (route_key ~ '^[a-z][a-z0-9-]{1,62}$'),
|
||||
display_name text not null
|
||||
check (length(btrim(display_name)) between 1 and 160),
|
||||
edge_id uuid not null references device_edges(id),
|
||||
model_profile_ref text not null references device_model_profiles(profile_ref),
|
||||
listener_ref text not null
|
||||
check (length(btrim(listener_ref)) between 3 and 256),
|
||||
protocol text not null
|
||||
check (protocol ~ '^[A-Z][A-Z0-9_]{0,31}$'),
|
||||
direction text not null default 'telemetry'
|
||||
check (direction in ('telemetry', 'bidirectional')),
|
||||
lifecycle_state text not null default 'draft'
|
||||
check (lifecycle_state in ('draft', 'active', 'suspended', 'retired')),
|
||||
created_by_ref text not null
|
||||
check (length(btrim(created_by_ref)) between 3 and 256),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (project_id, route_key),
|
||||
unique (id, project_id),
|
||||
unique (id, edge_id, project_id),
|
||||
unique (id, project_id, model_profile_ref)
|
||||
);
|
||||
|
||||
create index if not exists device_routes_edge_state_idx
|
||||
on device_routes (edge_id, lifecycle_state, updated_at desc);
|
||||
|
||||
create index if not exists device_routes_project_state_idx
|
||||
on device_routes (project_id, lifecycle_state, updated_at desc);
|
||||
|
||||
create table if not exists device_sessions (
|
||||
id uuid primary key,
|
||||
session_ref text not null
|
||||
check (length(btrim(session_ref)) between 3 and 256),
|
||||
edge_id uuid not null,
|
||||
project_id uuid not null,
|
||||
route_id uuid not null,
|
||||
device_id uuid,
|
||||
protocol text not null
|
||||
check (protocol ~ '^[A-Z][A-Z0-9_]{0,31}$'),
|
||||
lifecycle_state text not null default 'connecting'
|
||||
check (lifecycle_state in ('connecting', 'online', 'closing', 'closed', 'rejected')),
|
||||
connected_at timestamptz not null,
|
||||
last_seen_at timestamptz not null,
|
||||
disconnected_at timestamptz,
|
||||
close_reason_code text
|
||||
check (
|
||||
close_reason_code is null
|
||||
or close_reason_code ~ '^[a-z][a-z0-9._-]{1,63}$'
|
||||
),
|
||||
frame_count bigint not null default 0 check (frame_count >= 0),
|
||||
byte_count bigint not null default 0 check (byte_count >= 0),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (edge_id, session_ref),
|
||||
foreign key (route_id, edge_id, project_id)
|
||||
references device_routes(id, edge_id, project_id),
|
||||
foreign key (device_id, project_id)
|
||||
references device_instances(id, project_id),
|
||||
check (last_seen_at >= connected_at),
|
||||
check (
|
||||
(lifecycle_state in ('connecting', 'online') and disconnected_at is null)
|
||||
or
|
||||
(lifecycle_state in ('closing', 'closed', 'rejected'))
|
||||
)
|
||||
);
|
||||
|
||||
create index if not exists device_sessions_route_state_seen_idx
|
||||
on device_sessions (route_id, lifecycle_state, last_seen_at desc);
|
||||
|
||||
create index if not exists device_sessions_device_seen_idx
|
||||
on device_sessions (device_id, last_seen_at desc)
|
||||
where device_id is not null;
|
||||
|
||||
create table if not exists device_enrollment_intents (
|
||||
id uuid primary key,
|
||||
project_id uuid not null references device_projects(id),
|
||||
enrollment_key text not null
|
||||
check (enrollment_key ~ '^[a-z][a-z0-9-]{1,62}$'),
|
||||
route_id uuid not null,
|
||||
model_profile_ref text not null references device_model_profiles(profile_ref),
|
||||
display_name text not null
|
||||
check (length(btrim(display_name)) between 1 and 160),
|
||||
expected_identifier_kind text not null
|
||||
check (expected_identifier_kind ~ '^[a-z][a-z0-9._-]{1,31}$'),
|
||||
expected_identifier_digest text not null
|
||||
check (expected_identifier_digest ~ '^hmac-sha256:[a-f0-9]{64}$'),
|
||||
expected_identifier_masked text not null
|
||||
check (length(btrim(expected_identifier_masked)) between 4 and 64),
|
||||
lifecycle_state text not null default 'pending'
|
||||
check (lifecycle_state in ('pending', 'observed', 'claimed', 'cancelled', 'expired')),
|
||||
expires_at timestamptz,
|
||||
claimed_device_id uuid,
|
||||
created_by_ref text not null
|
||||
check (length(btrim(created_by_ref)) between 3 and 256),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (project_id, enrollment_key),
|
||||
unique (
|
||||
project_id,
|
||||
expected_identifier_kind,
|
||||
expected_identifier_digest,
|
||||
model_profile_ref
|
||||
),
|
||||
foreign key (route_id, project_id, model_profile_ref)
|
||||
references device_routes(id, project_id, model_profile_ref),
|
||||
foreign key (claimed_device_id, project_id)
|
||||
references device_instances(id, project_id),
|
||||
check (expires_at is null or expires_at > created_at)
|
||||
);
|
||||
|
||||
create index if not exists device_enrollment_intents_project_state_idx
|
||||
on device_enrollment_intents (project_id, lifecycle_state, updated_at desc);
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,21 @@
|
||||
begin;
|
||||
|
||||
alter table device_management_command_receipts
|
||||
drop constraint if exists device_management_command_receipts_command_kind_check;
|
||||
|
||||
alter table device_management_command_receipts
|
||||
add constraint device_management_command_receipts_command_kind_check
|
||||
check (command_kind in (
|
||||
'owner_scope.ensure',
|
||||
'project.ensure',
|
||||
'collection.ensure',
|
||||
'project_grant.upsert',
|
||||
'adapter_package.ensure',
|
||||
'adapter_version.register',
|
||||
'model_profile.register',
|
||||
'edge.ensure',
|
||||
'route.ensure',
|
||||
'enrollment_intent.ensure'
|
||||
));
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,326 @@
|
||||
begin;
|
||||
|
||||
create unique index if not exists device_projects_id_owner_scope_idx
|
||||
on device_projects (id, owner_scope_id);
|
||||
|
||||
alter table device_instances
|
||||
alter column contour_id drop not null,
|
||||
add column if not exists owner_scope_id uuid references device_owner_scopes(id),
|
||||
add column if not exists device_key text
|
||||
check (
|
||||
device_key is null
|
||||
or device_key ~ '^[a-z][a-z0-9-]{1,62}$'
|
||||
);
|
||||
|
||||
create unique index if not exists device_instances_project_key_idx
|
||||
on device_instances (project_id, device_key)
|
||||
where device_key is not null;
|
||||
|
||||
create unique index if not exists device_instances_id_project_owner_idx
|
||||
on device_instances (id, project_id, owner_scope_id);
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_instances_project_owner_fk'
|
||||
and conrelid = 'device_instances'::regclass
|
||||
) then
|
||||
alter table device_instances
|
||||
add constraint device_instances_project_owner_fk
|
||||
foreign key (project_id, owner_scope_id)
|
||||
references device_projects(id, owner_scope_id)
|
||||
not valid;
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_instances_ownership_mode_check'
|
||||
and conrelid = 'device_instances'::regclass
|
||||
) then
|
||||
alter table device_instances
|
||||
add constraint device_instances_ownership_mode_check
|
||||
check (
|
||||
(
|
||||
owner_scope_id is not null
|
||||
and project_id is not null
|
||||
)
|
||||
or
|
||||
(
|
||||
owner_scope_id is null
|
||||
and project_id is null
|
||||
and contour_id is not null
|
||||
)
|
||||
) not valid;
|
||||
end if;
|
||||
end
|
||||
$$;
|
||||
|
||||
alter table device_discoveries
|
||||
add column if not exists session_ref text
|
||||
check (
|
||||
session_ref is null
|
||||
or length(btrim(session_ref)) between 3 and 256
|
||||
),
|
||||
add column if not exists project_id uuid references device_projects(id),
|
||||
add column if not exists route_id uuid,
|
||||
add column if not exists enrollment_intent_id uuid,
|
||||
add column if not exists resolution_code text
|
||||
check (
|
||||
resolution_code is null
|
||||
or resolution_code ~ '^[a-z][a-z0-9._-]{1,63}$'
|
||||
),
|
||||
add column if not exists resolved_at timestamptz,
|
||||
add column if not exists resolved_by_ref text
|
||||
check (
|
||||
resolved_by_ref is null
|
||||
or length(btrim(resolved_by_ref)) between 3 and 256
|
||||
);
|
||||
|
||||
create unique index if not exists device_discoveries_id_project_idx
|
||||
on device_discoveries (id, project_id);
|
||||
|
||||
create index if not exists device_discoveries_route_state_seen_idx
|
||||
on device_discoveries (route_id, lifecycle_state, last_observed_at desc)
|
||||
where route_id is not null;
|
||||
|
||||
create unique index if not exists device_enrollment_intents_context_idx
|
||||
on device_enrollment_intents (
|
||||
id,
|
||||
project_id,
|
||||
route_id,
|
||||
model_profile_ref
|
||||
);
|
||||
|
||||
create unique index if not exists device_enrollment_intents_active_identity_idx
|
||||
on device_enrollment_intents (
|
||||
expected_identifier_kind,
|
||||
expected_identifier_digest,
|
||||
model_profile_ref
|
||||
)
|
||||
where lifecycle_state in ('pending', 'observed', 'claimed');
|
||||
|
||||
alter table device_enrollment_intents
|
||||
add column if not exists observed_discovery_id uuid,
|
||||
add column if not exists observed_at timestamptz,
|
||||
add column if not exists claimed_at timestamptz,
|
||||
add column if not exists resolution_code text
|
||||
check (
|
||||
resolution_code is null
|
||||
or resolution_code ~ '^[a-z][a-z0-9._-]{1,63}$'
|
||||
),
|
||||
add column if not exists resolved_at timestamptz,
|
||||
add column if not exists resolved_by_ref text
|
||||
check (
|
||||
resolved_by_ref is null
|
||||
or length(btrim(resolved_by_ref)) between 3 and 256
|
||||
);
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_discoveries_route_context_fk'
|
||||
and conrelid = 'device_discoveries'::regclass
|
||||
) then
|
||||
alter table device_discoveries
|
||||
add constraint device_discoveries_route_context_fk
|
||||
foreign key (route_id, project_id, model_profile_ref)
|
||||
references device_routes(id, project_id, model_profile_ref)
|
||||
not valid;
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_discoveries_enrollment_context_fk'
|
||||
and conrelid = 'device_discoveries'::regclass
|
||||
) then
|
||||
alter table device_discoveries
|
||||
add constraint device_discoveries_enrollment_context_fk
|
||||
foreign key (
|
||||
enrollment_intent_id,
|
||||
project_id,
|
||||
route_id,
|
||||
model_profile_ref
|
||||
) references device_enrollment_intents (
|
||||
id,
|
||||
project_id,
|
||||
route_id,
|
||||
model_profile_ref
|
||||
) not valid;
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_discoveries_route_context_check'
|
||||
and conrelid = 'device_discoveries'::regclass
|
||||
) then
|
||||
alter table device_discoveries
|
||||
add constraint device_discoveries_route_context_check
|
||||
check (
|
||||
(project_id is null and route_id is null)
|
||||
or
|
||||
(project_id is not null and route_id is not null)
|
||||
) not valid;
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_discoveries_enrollment_context_check'
|
||||
and conrelid = 'device_discoveries'::regclass
|
||||
) then
|
||||
alter table device_discoveries
|
||||
add constraint device_discoveries_enrollment_context_check
|
||||
check (
|
||||
enrollment_intent_id is null
|
||||
or (project_id is not null and route_id is not null)
|
||||
) not valid;
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_enrollment_observed_discovery_fk'
|
||||
and conrelid = 'device_enrollment_intents'::regclass
|
||||
) then
|
||||
alter table device_enrollment_intents
|
||||
add constraint device_enrollment_observed_discovery_fk
|
||||
foreign key (observed_discovery_id, project_id)
|
||||
references device_discoveries(id, project_id)
|
||||
not valid;
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_enrollment_lifecycle_evidence_check'
|
||||
and conrelid = 'device_enrollment_intents'::regclass
|
||||
) then
|
||||
alter table device_enrollment_intents
|
||||
add constraint device_enrollment_lifecycle_evidence_check
|
||||
check (
|
||||
lifecycle_state not in ('observed', 'claimed')
|
||||
or (observed_discovery_id is not null and observed_at is not null)
|
||||
) not valid;
|
||||
end if;
|
||||
end
|
||||
$$;
|
||||
|
||||
alter table device_sessions
|
||||
drop constraint if exists device_sessions_device_id_project_id_fkey;
|
||||
|
||||
alter table device_enrollment_intents
|
||||
drop constraint if exists device_enrollment_intents_claimed_device_id_project_id_fkey;
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_sessions_device_id_fk'
|
||||
and conrelid = 'device_sessions'::regclass
|
||||
) then
|
||||
alter table device_sessions
|
||||
add constraint device_sessions_device_id_fk
|
||||
foreign key (device_id) references device_instances(id)
|
||||
not valid;
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_enrollment_claimed_device_id_fk'
|
||||
and conrelid = 'device_enrollment_intents'::regclass
|
||||
) then
|
||||
alter table device_enrollment_intents
|
||||
add constraint device_enrollment_claimed_device_id_fk
|
||||
foreign key (claimed_device_id) references device_instances(id)
|
||||
not valid;
|
||||
end if;
|
||||
end
|
||||
$$;
|
||||
|
||||
create or replace function device_assert_session_current_project()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if new.device_id is not null and not exists (
|
||||
select 1 from device_instances di
|
||||
where di.id = new.device_id
|
||||
and di.project_id = new.project_id
|
||||
) then
|
||||
raise foreign_key_violation using
|
||||
message = 'device_session_project_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_sessions_current_project_guard
|
||||
on device_sessions;
|
||||
|
||||
create trigger device_sessions_current_project_guard
|
||||
before insert or update of device_id, project_id
|
||||
on device_sessions
|
||||
for each row
|
||||
execute function device_assert_session_current_project();
|
||||
|
||||
create or replace function device_assert_enrollment_current_project()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if new.claimed_device_id is not null and not exists (
|
||||
select 1 from device_instances di
|
||||
where di.id = new.claimed_device_id
|
||||
and di.project_id = new.project_id
|
||||
) then
|
||||
raise foreign_key_violation using
|
||||
message = 'device_enrollment_project_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_enrollment_current_project_guard
|
||||
on device_enrollment_intents;
|
||||
|
||||
create trigger device_enrollment_current_project_guard
|
||||
before insert or update of claimed_device_id, project_id
|
||||
on device_enrollment_intents
|
||||
for each row
|
||||
execute function device_assert_enrollment_current_project();
|
||||
|
||||
create table if not exists device_ownership_transitions (
|
||||
id uuid primary key,
|
||||
device_id uuid not null references device_instances(id),
|
||||
transition_kind text not null
|
||||
check (transition_kind in ('claim', 'transfer')),
|
||||
source_owner_scope_id uuid,
|
||||
source_project_id uuid,
|
||||
target_owner_scope_id uuid not null,
|
||||
target_project_id uuid not null,
|
||||
actor_ref text not null
|
||||
check (length(btrim(actor_ref)) between 3 and 256),
|
||||
occurred_at timestamptz not null default now(),
|
||||
foreign key (source_project_id, source_owner_scope_id)
|
||||
references device_projects(id, owner_scope_id),
|
||||
foreign key (target_project_id, target_owner_scope_id)
|
||||
references device_projects(id, owner_scope_id),
|
||||
check (
|
||||
(
|
||||
transition_kind = 'claim'
|
||||
and source_owner_scope_id is null
|
||||
and source_project_id is null
|
||||
)
|
||||
or
|
||||
(
|
||||
transition_kind = 'transfer'
|
||||
and source_owner_scope_id is not null
|
||||
and source_project_id is not null
|
||||
and (
|
||||
source_owner_scope_id <> target_owner_scope_id
|
||||
or source_project_id <> target_project_id
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
create unique index if not exists device_ownership_single_claim_idx
|
||||
on device_ownership_transitions (device_id)
|
||||
where transition_kind = 'claim';
|
||||
|
||||
create index if not exists device_ownership_device_time_idx
|
||||
on device_ownership_transitions (device_id, occurred_at desc);
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,25 @@
|
||||
begin;
|
||||
|
||||
alter table device_management_command_receipts
|
||||
drop constraint if exists device_management_command_receipts_command_kind_check;
|
||||
|
||||
alter table device_management_command_receipts
|
||||
add constraint device_management_command_receipts_command_kind_check
|
||||
check (command_kind in (
|
||||
'owner_scope.ensure',
|
||||
'project.ensure',
|
||||
'collection.ensure',
|
||||
'project_grant.upsert',
|
||||
'adapter_package.ensure',
|
||||
'adapter_version.register',
|
||||
'model_profile.register',
|
||||
'edge.ensure',
|
||||
'route.ensure',
|
||||
'enrollment_intent.ensure',
|
||||
'device.claim',
|
||||
'device.transfer',
|
||||
'discovery.reject',
|
||||
'discovery.expire'
|
||||
));
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,233 @@
|
||||
begin;
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_instances_direct_legacy_credential_check'
|
||||
and conrelid = 'device_instances'::regclass
|
||||
) then
|
||||
alter table device_instances
|
||||
add constraint device_instances_direct_legacy_credential_check
|
||||
check (owner_scope_id is null or credential_ref is null)
|
||||
not valid;
|
||||
end if;
|
||||
end
|
||||
$$;
|
||||
|
||||
create table if not exists device_restricted_identifiers (
|
||||
id uuid primary key,
|
||||
device_id uuid not null references device_instances(id),
|
||||
owner_scope_id uuid not null,
|
||||
project_id uuid not null,
|
||||
identifier_kind text not null
|
||||
check (identifier_kind ~ '^[a-z][a-z0-9._:-]{1,63}$'),
|
||||
identifier_digest text not null
|
||||
check (identifier_digest ~ '^hmac-sha256:[a-f0-9]{64}$'),
|
||||
identifier_masked text not null
|
||||
check (
|
||||
length(identifier_masked) between 5 and 128
|
||||
and position('*' in identifier_masked) > 0
|
||||
and identifier_masked !~ '[[:cntrl:]]'
|
||||
and identifier_masked !~ '(^|[^0-9])[0-9]{15}([^0-9]|$)'
|
||||
),
|
||||
provenance_kind text not null
|
||||
check (provenance_kind in ('claim', 'adapter_observation')),
|
||||
is_primary boolean not null default false,
|
||||
lifecycle_state text not null default 'active'
|
||||
check (lifecycle_state in ('active', 'revoked')),
|
||||
created_by_ref text not null
|
||||
check (length(btrim(created_by_ref)) between 3 and 256),
|
||||
revoked_at timestamptz,
|
||||
revoked_by_ref text
|
||||
check (
|
||||
revoked_by_ref is null
|
||||
or length(btrim(revoked_by_ref)) between 3 and 256
|
||||
),
|
||||
revocation_code text
|
||||
check (
|
||||
revocation_code is null
|
||||
or revocation_code ~ '^[a-z][a-z0-9._-]{1,63}$'
|
||||
),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
foreign key (project_id, owner_scope_id)
|
||||
references device_projects(id, owner_scope_id),
|
||||
check (
|
||||
(lifecycle_state = 'active' and revoked_at is null and revoked_by_ref is null and revocation_code is null)
|
||||
or
|
||||
(lifecycle_state = 'revoked' and revoked_at is not null and revoked_by_ref is not null and revocation_code is not null)
|
||||
)
|
||||
);
|
||||
|
||||
create unique index if not exists device_restricted_identifiers_active_identity_idx
|
||||
on device_restricted_identifiers (identifier_kind, identifier_digest)
|
||||
where lifecycle_state = 'active';
|
||||
|
||||
create unique index if not exists device_restricted_identifiers_primary_idx
|
||||
on device_restricted_identifiers (device_id)
|
||||
where lifecycle_state = 'active' and is_primary;
|
||||
|
||||
create index if not exists device_restricted_identifiers_device_idx
|
||||
on device_restricted_identifiers (device_id, lifecycle_state, created_at);
|
||||
|
||||
create or replace function device_assert_identifier_current_owner()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if new.lifecycle_state = 'active' and not exists (
|
||||
select 1 from device_instances di
|
||||
where di.id = new.device_id
|
||||
and di.owner_scope_id = new.owner_scope_id
|
||||
and di.project_id = new.project_id
|
||||
) then
|
||||
raise foreign_key_violation using
|
||||
message = 'device_identifier_ownership_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_restricted_identifiers_owner_guard
|
||||
on device_restricted_identifiers;
|
||||
|
||||
create trigger device_restricted_identifiers_owner_guard
|
||||
before insert or update of device_id, owner_scope_id, project_id, lifecycle_state
|
||||
on device_restricted_identifiers
|
||||
for each row
|
||||
execute function device_assert_identifier_current_owner();
|
||||
|
||||
create or replace function device_assert_active_identifiers_follow_owner()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if exists (
|
||||
select 1 from device_restricted_identifiers dri
|
||||
where dri.device_id = new.id
|
||||
and dri.lifecycle_state = 'active'
|
||||
and (
|
||||
dri.owner_scope_id is distinct from new.owner_scope_id
|
||||
or dri.project_id is distinct from new.project_id
|
||||
)
|
||||
) then
|
||||
raise foreign_key_violation using
|
||||
message = 'device_active_identifier_ownership_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_instances_identifier_owner_guard
|
||||
on device_instances;
|
||||
|
||||
create constraint trigger device_instances_identifier_owner_guard
|
||||
after update
|
||||
on device_instances
|
||||
deferrable initially deferred
|
||||
for each row
|
||||
execute function device_assert_active_identifiers_follow_owner();
|
||||
|
||||
create table if not exists device_credential_bindings (
|
||||
id uuid primary key,
|
||||
device_id uuid not null references device_instances(id),
|
||||
owner_scope_id uuid not null,
|
||||
project_id uuid not null,
|
||||
purpose text not null
|
||||
check (purpose ~ '^[a-z][a-z0-9._-]{1,63}$'),
|
||||
credential_owner text not null
|
||||
check (credential_owner = 'ndc_l2_credentials'),
|
||||
credential_ref text not null
|
||||
check (credential_ref ~ '^ndc-credref:[A-Za-z0-9][A-Za-z0-9._:-]{7,240}$'),
|
||||
lifecycle_state text not null default 'active'
|
||||
check (lifecycle_state in ('active', 'revoked')),
|
||||
bound_by_ref text not null
|
||||
check (length(btrim(bound_by_ref)) between 3 and 256),
|
||||
revoked_at timestamptz,
|
||||
revoked_by_ref text
|
||||
check (
|
||||
revoked_by_ref is null
|
||||
or length(btrim(revoked_by_ref)) between 3 and 256
|
||||
),
|
||||
revocation_code text
|
||||
check (
|
||||
revocation_code is null
|
||||
or revocation_code ~ '^[a-z][a-z0-9._-]{1,63}$'
|
||||
),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
foreign key (project_id, owner_scope_id)
|
||||
references device_projects(id, owner_scope_id),
|
||||
check (
|
||||
(lifecycle_state = 'active' and revoked_at is null and revoked_by_ref is null and revocation_code is null)
|
||||
or
|
||||
(lifecycle_state = 'revoked' and revoked_at is not null and revoked_by_ref is not null and revocation_code is not null)
|
||||
)
|
||||
);
|
||||
|
||||
create unique index if not exists device_credential_bindings_active_purpose_idx
|
||||
on device_credential_bindings (device_id, purpose)
|
||||
where lifecycle_state = 'active';
|
||||
|
||||
create index if not exists device_credential_bindings_project_state_idx
|
||||
on device_credential_bindings (project_id, lifecycle_state, updated_at desc);
|
||||
|
||||
create or replace function device_assert_credential_binding_current_owner()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if new.lifecycle_state = 'active' and not exists (
|
||||
select 1 from device_instances di
|
||||
where di.id = new.device_id
|
||||
and di.owner_scope_id = new.owner_scope_id
|
||||
and di.project_id = new.project_id
|
||||
) then
|
||||
raise foreign_key_violation using
|
||||
message = 'device_credential_binding_ownership_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_credential_bindings_owner_guard
|
||||
on device_credential_bindings;
|
||||
|
||||
create trigger device_credential_bindings_owner_guard
|
||||
before insert or update of device_id, owner_scope_id, project_id, lifecycle_state
|
||||
on device_credential_bindings
|
||||
for each row
|
||||
execute function device_assert_credential_binding_current_owner();
|
||||
|
||||
create or replace function device_require_credential_revoke_before_transfer()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if exists (
|
||||
select 1 from device_credential_bindings dcb
|
||||
where dcb.device_id = old.id
|
||||
and dcb.lifecycle_state = 'active'
|
||||
) then
|
||||
raise check_violation using
|
||||
message = 'device_transfer_active_credential_binding';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_instances_credential_transfer_guard
|
||||
on device_instances;
|
||||
|
||||
create trigger device_instances_credential_transfer_guard
|
||||
before update of owner_scope_id, project_id
|
||||
on device_instances
|
||||
for each row
|
||||
when (
|
||||
old.owner_scope_id is distinct from new.owner_scope_id
|
||||
or old.project_id is distinct from new.project_id
|
||||
)
|
||||
execute function device_require_credential_revoke_before_transfer();
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,27 @@
|
||||
begin;
|
||||
|
||||
alter table device_management_command_receipts
|
||||
drop constraint if exists device_management_command_receipts_command_kind_check;
|
||||
|
||||
alter table device_management_command_receipts
|
||||
add constraint device_management_command_receipts_command_kind_check
|
||||
check (command_kind in (
|
||||
'owner_scope.ensure',
|
||||
'project.ensure',
|
||||
'collection.ensure',
|
||||
'project_grant.upsert',
|
||||
'adapter_package.ensure',
|
||||
'adapter_version.register',
|
||||
'model_profile.register',
|
||||
'edge.ensure',
|
||||
'route.ensure',
|
||||
'enrollment_intent.ensure',
|
||||
'device.claim',
|
||||
'device.transfer',
|
||||
'discovery.reject',
|
||||
'discovery.expire',
|
||||
'device_credential_binding.upsert',
|
||||
'device_credential_binding.revoke'
|
||||
));
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,610 @@
|
||||
begin;
|
||||
|
||||
create table if not exists device_resource_bindings (
|
||||
id uuid primary key,
|
||||
owner_scope_id uuid not null,
|
||||
project_id uuid not null,
|
||||
binding_key text not null
|
||||
check (binding_key ~ '^[a-z][a-z0-9-]{1,62}$'),
|
||||
display_name text not null
|
||||
check (length(btrim(display_name)) between 1 and 160),
|
||||
source_kind text not null
|
||||
check (source_kind in ('device', 'collection')),
|
||||
device_id uuid references device_instances(id),
|
||||
collection_id uuid references device_collections(id),
|
||||
target_kind text not null
|
||||
check (target_kind ~ '^[a-z][a-z0-9._:-]{1,63}$'),
|
||||
target_ref text not null
|
||||
check (length(btrim(target_ref)) between 3 and 256),
|
||||
capabilities text[] not null default '{}'
|
||||
check (
|
||||
cardinality(capabilities) between 1 and 16
|
||||
and array_position(capabilities, null) is null
|
||||
),
|
||||
lifecycle_state text not null default 'pending_external_approval'
|
||||
check (lifecycle_state in ('pending_external_approval', 'active', 'revoked')),
|
||||
source_approved_by_ref text not null
|
||||
check (length(btrim(source_approved_by_ref)) between 3 and 256),
|
||||
source_approved_at timestamptz not null default now(),
|
||||
external_approval_ref text
|
||||
check (
|
||||
external_approval_ref is null
|
||||
or length(btrim(external_approval_ref)) between 3 and 256
|
||||
),
|
||||
external_approval_digest text
|
||||
check (
|
||||
external_approval_digest is null
|
||||
or external_approval_digest ~ '^sha256:[a-f0-9]{64}$'
|
||||
),
|
||||
external_approved_at timestamptz,
|
||||
revoked_at timestamptz,
|
||||
revoked_by_ref text
|
||||
check (
|
||||
revoked_by_ref is null
|
||||
or length(btrim(revoked_by_ref)) between 3 and 256
|
||||
),
|
||||
revocation_code text
|
||||
check (
|
||||
revocation_code is null
|
||||
or revocation_code ~ '^[a-z][a-z0-9._-]{1,63}$'
|
||||
),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (project_id, binding_key),
|
||||
foreign key (project_id, owner_scope_id)
|
||||
references device_projects(id, owner_scope_id),
|
||||
foreign key (collection_id, project_id)
|
||||
references device_collections(id, project_id),
|
||||
check (
|
||||
(source_kind = 'device' and device_id is not null and collection_id is null)
|
||||
or
|
||||
(source_kind = 'collection' and device_id is null and collection_id is not null)
|
||||
),
|
||||
check (
|
||||
(
|
||||
lifecycle_state = 'pending_external_approval'
|
||||
and external_approval_ref is null
|
||||
and external_approval_digest is null
|
||||
and external_approved_at is null
|
||||
and revoked_at is null
|
||||
and revoked_by_ref is null
|
||||
and revocation_code is null
|
||||
)
|
||||
or
|
||||
(
|
||||
lifecycle_state = 'active'
|
||||
and external_approval_ref is not null
|
||||
and external_approval_digest is not null
|
||||
and external_approved_at is not null
|
||||
and revoked_at is null
|
||||
and revoked_by_ref is null
|
||||
and revocation_code is null
|
||||
)
|
||||
or
|
||||
(
|
||||
lifecycle_state = 'revoked'
|
||||
and revoked_at is not null
|
||||
and revoked_by_ref is not null
|
||||
and revocation_code is not null
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
create index if not exists device_resource_bindings_project_state_idx
|
||||
on device_resource_bindings (project_id, lifecycle_state, updated_at desc);
|
||||
|
||||
create index if not exists device_resource_bindings_device_state_idx
|
||||
on device_resource_bindings (device_id, lifecycle_state, updated_at desc)
|
||||
where device_id is not null;
|
||||
|
||||
create or replace function device_assert_binding_source_scope()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if new.source_kind = 'device' and not exists (
|
||||
select 1 from device_instances di
|
||||
where di.id = new.device_id
|
||||
and di.owner_scope_id = new.owner_scope_id
|
||||
and di.project_id = new.project_id
|
||||
) then
|
||||
raise foreign_key_violation using
|
||||
message = 'device_binding_source_scope_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_resource_bindings_source_guard
|
||||
on device_resource_bindings;
|
||||
|
||||
create trigger device_resource_bindings_source_guard
|
||||
before insert or update of owner_scope_id, project_id, source_kind, device_id, collection_id
|
||||
on device_resource_bindings
|
||||
for each row
|
||||
execute function device_assert_binding_source_scope();
|
||||
|
||||
create table if not exists device_configuration_revisions (
|
||||
id uuid primary key,
|
||||
owner_scope_id uuid not null,
|
||||
project_id uuid not null,
|
||||
device_id uuid not null references device_instances(id),
|
||||
revision_number bigint not null check (revision_number > 0),
|
||||
model_profile_ref text not null references device_model_profiles(profile_ref),
|
||||
schema_artifact_ref text not null
|
||||
check (length(btrim(schema_artifact_ref)) between 3 and 256),
|
||||
configuration_digest text not null
|
||||
check (configuration_digest ~ '^sha256:[a-f0-9]{64}$'),
|
||||
configuration jsonb not null
|
||||
check (
|
||||
jsonb_typeof(configuration) = 'object'
|
||||
and octet_length(configuration::text) <= 65536
|
||||
),
|
||||
change_summary text
|
||||
check (change_summary is null or length(change_summary) <= 1000),
|
||||
created_by_ref text not null
|
||||
check (length(btrim(created_by_ref)) between 3 and 256),
|
||||
created_at timestamptz not null default now(),
|
||||
unique (device_id, revision_number),
|
||||
unique (id, device_id, project_id),
|
||||
foreign key (project_id, owner_scope_id)
|
||||
references device_projects(id, owner_scope_id)
|
||||
);
|
||||
|
||||
create index if not exists device_configuration_revisions_project_idx
|
||||
on device_configuration_revisions (project_id, device_id, revision_number desc);
|
||||
|
||||
create or replace function device_assert_configuration_revision_scope()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1
|
||||
from device_instances di
|
||||
join device_model_profiles dmp
|
||||
on dmp.profile_ref = di.model_profile_ref
|
||||
where di.id = new.device_id
|
||||
and di.owner_scope_id = new.owner_scope_id
|
||||
and di.project_id = new.project_id
|
||||
and di.model_profile_ref = new.model_profile_ref
|
||||
and dmp.schema_artifact_ref = new.schema_artifact_ref
|
||||
and dmp.lifecycle_state = 'active'
|
||||
) then
|
||||
raise foreign_key_violation using
|
||||
message = 'device_configuration_revision_scope_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_configuration_revisions_scope_guard
|
||||
on device_configuration_revisions;
|
||||
|
||||
create trigger device_configuration_revisions_scope_guard
|
||||
before insert
|
||||
on device_configuration_revisions
|
||||
for each row
|
||||
execute function device_assert_configuration_revision_scope();
|
||||
|
||||
create table if not exists device_configuration_state (
|
||||
device_id uuid primary key,
|
||||
owner_scope_id uuid not null,
|
||||
project_id uuid not null,
|
||||
desired_revision_id uuid,
|
||||
applied_revision_id uuid,
|
||||
applied_at timestamptz,
|
||||
applied_by_ref text,
|
||||
updated_at timestamptz not null default now(),
|
||||
foreign key (device_id, project_id, owner_scope_id)
|
||||
references device_instances(id, project_id, owner_scope_id),
|
||||
foreign key (desired_revision_id, device_id, project_id)
|
||||
references device_configuration_revisions(id, device_id, project_id),
|
||||
foreign key (applied_revision_id, device_id, project_id)
|
||||
references device_configuration_revisions(id, device_id, project_id),
|
||||
check (desired_revision_id is not null or applied_revision_id is not null),
|
||||
check (
|
||||
(applied_revision_id is null and applied_at is null and applied_by_ref is null)
|
||||
or
|
||||
(applied_revision_id is not null and applied_at is not null and applied_by_ref is not null)
|
||||
)
|
||||
);
|
||||
|
||||
create table if not exists device_commands (
|
||||
id uuid primary key,
|
||||
owner_scope_id uuid not null,
|
||||
project_id uuid not null,
|
||||
device_id uuid not null references device_instances(id),
|
||||
command_key text not null
|
||||
check (command_key ~ '^[a-z][a-z0-9-]{1,62}$'),
|
||||
command_catalog_ref text not null
|
||||
check (length(btrim(command_catalog_ref)) between 3 and 256),
|
||||
command_type text not null
|
||||
check (command_type ~ '^[a-z][a-z0-9._:-]{1,63}$'),
|
||||
risk_class text not null
|
||||
check (risk_class in ('low', 'moderate', 'high', 'critical')),
|
||||
parameters_digest text not null
|
||||
check (parameters_digest ~ '^sha256:[a-f0-9]{64}$'),
|
||||
parameters_projection jsonb not null
|
||||
check (
|
||||
jsonb_typeof(parameters_projection) = 'object'
|
||||
and octet_length(parameters_projection::text) <= 16384
|
||||
),
|
||||
lifecycle_state text not null default 'draft'
|
||||
check (lifecycle_state in (
|
||||
'draft',
|
||||
'planned',
|
||||
'awaiting_confirmation',
|
||||
'queued',
|
||||
'dispatched',
|
||||
'acknowledged',
|
||||
'verified',
|
||||
'failed',
|
||||
'expired',
|
||||
'unknown'
|
||||
)),
|
||||
planned_by_ref text not null
|
||||
check (length(btrim(planned_by_ref)) between 3 and 256),
|
||||
planned_at timestamptz not null default now(),
|
||||
expires_at timestamptz not null,
|
||||
confirmed_by_ref text,
|
||||
confirmed_at timestamptz,
|
||||
dispatched_at timestamptz,
|
||||
transport_message_ref text
|
||||
check (
|
||||
transport_message_ref is null
|
||||
or length(btrim(transport_message_ref)) between 3 and 256
|
||||
),
|
||||
acknowledged_at timestamptz,
|
||||
terminal_at timestamptz,
|
||||
terminal_reason_code text
|
||||
check (
|
||||
terminal_reason_code is null
|
||||
or terminal_reason_code ~ '^[a-z][a-z0-9._-]{1,63}$'
|
||||
),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (project_id, command_key),
|
||||
unique (id, device_id, project_id),
|
||||
foreign key (project_id, owner_scope_id)
|
||||
references device_projects(id, owner_scope_id),
|
||||
check (expires_at > planned_at),
|
||||
check (
|
||||
(confirmed_at is null and confirmed_by_ref is null)
|
||||
or
|
||||
(confirmed_at is not null and confirmed_by_ref is not null)
|
||||
),
|
||||
check (
|
||||
(
|
||||
lifecycle_state in ('dispatched', 'acknowledged', 'verified', 'unknown')
|
||||
and dispatched_at is not null
|
||||
and transport_message_ref is not null
|
||||
)
|
||||
or lifecycle_state not in ('dispatched', 'acknowledged', 'verified', 'unknown')
|
||||
),
|
||||
check (
|
||||
(lifecycle_state in ('acknowledged', 'verified') and acknowledged_at is not null)
|
||||
or lifecycle_state not in ('acknowledged', 'verified')
|
||||
),
|
||||
check (
|
||||
(
|
||||
lifecycle_state in ('verified', 'failed', 'expired', 'unknown')
|
||||
and terminal_at is not null
|
||||
and terminal_reason_code is not null
|
||||
)
|
||||
or
|
||||
(
|
||||
lifecycle_state not in ('verified', 'failed', 'expired', 'unknown')
|
||||
and terminal_at is null
|
||||
and terminal_reason_code is null
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
create index if not exists device_commands_project_state_idx
|
||||
on device_commands (project_id, lifecycle_state, updated_at desc);
|
||||
|
||||
create index if not exists device_commands_device_state_idx
|
||||
on device_commands (device_id, lifecycle_state, updated_at desc);
|
||||
|
||||
create table if not exists device_command_events (
|
||||
id uuid primary key,
|
||||
command_id uuid not null,
|
||||
device_id uuid not null,
|
||||
project_id uuid not null,
|
||||
sequence_number bigint not null check (sequence_number > 0),
|
||||
from_state text,
|
||||
to_state text not null
|
||||
check (to_state in (
|
||||
'draft',
|
||||
'planned',
|
||||
'awaiting_confirmation',
|
||||
'queued',
|
||||
'dispatched',
|
||||
'acknowledged',
|
||||
'verified',
|
||||
'failed',
|
||||
'expired',
|
||||
'unknown'
|
||||
)),
|
||||
actor_ref text not null
|
||||
check (length(btrim(actor_ref)) between 3 and 256),
|
||||
reason_code text not null
|
||||
check (reason_code ~ '^[a-z][a-z0-9._-]{1,63}$'),
|
||||
evidence_ref text
|
||||
check (
|
||||
evidence_ref is null
|
||||
or length(btrim(evidence_ref)) between 3 and 256
|
||||
),
|
||||
occurred_at timestamptz not null default now(),
|
||||
unique (command_id, sequence_number),
|
||||
foreign key (command_id, device_id, project_id)
|
||||
references device_commands(id, device_id, project_id),
|
||||
check (sequence_number = 1 or from_state is not null),
|
||||
check (sequence_number <> 1 or from_state is null),
|
||||
check (
|
||||
from_state is null
|
||||
or from_state in (
|
||||
'draft',
|
||||
'planned',
|
||||
'awaiting_confirmation',
|
||||
'queued',
|
||||
'dispatched',
|
||||
'acknowledged',
|
||||
'verified',
|
||||
'failed',
|
||||
'expired',
|
||||
'unknown'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
create index if not exists device_command_events_command_idx
|
||||
on device_command_events (command_id, sequence_number);
|
||||
|
||||
create or replace function device_assert_command_scope()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from device_instances di
|
||||
where di.id = new.device_id
|
||||
and di.owner_scope_id = new.owner_scope_id
|
||||
and di.project_id = new.project_id
|
||||
) then
|
||||
raise foreign_key_violation using message = 'device_command_scope_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_commands_scope_guard on device_commands;
|
||||
|
||||
create trigger device_commands_scope_guard
|
||||
before insert
|
||||
on device_commands
|
||||
for each row
|
||||
execute function device_assert_command_scope();
|
||||
|
||||
create or replace function device_assert_command_event_sequence()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
previous_state text;
|
||||
begin
|
||||
if new.sequence_number = 1 then
|
||||
if new.from_state is not null or new.to_state <> 'draft' then
|
||||
raise check_violation using message = 'device_command_initial_event_invalid';
|
||||
end if;
|
||||
return new;
|
||||
end if;
|
||||
|
||||
select dce.to_state into previous_state
|
||||
from device_command_events dce
|
||||
where dce.command_id = new.command_id
|
||||
and dce.sequence_number = new.sequence_number - 1;
|
||||
|
||||
if previous_state is null or previous_state <> new.from_state then
|
||||
raise check_violation using message = 'device_command_event_sequence_invalid';
|
||||
end if;
|
||||
if not (
|
||||
(new.from_state = 'draft' and new.to_state in ('planned', 'expired'))
|
||||
or (new.from_state = 'planned' and new.to_state in ('awaiting_confirmation', 'queued', 'expired'))
|
||||
or (new.from_state = 'awaiting_confirmation' and new.to_state in ('queued', 'expired'))
|
||||
or (new.from_state = 'queued' and new.to_state in ('dispatched', 'failed', 'expired'))
|
||||
or (new.from_state = 'dispatched' and new.to_state in ('acknowledged', 'failed', 'unknown'))
|
||||
or (new.from_state = 'acknowledged' and new.to_state in ('verified', 'failed', 'unknown'))
|
||||
) then
|
||||
raise check_violation using message = 'device_command_event_transition_invalid';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_command_events_sequence_guard
|
||||
on device_command_events;
|
||||
|
||||
create trigger device_command_events_sequence_guard
|
||||
before insert
|
||||
on device_command_events
|
||||
for each row
|
||||
execute function device_assert_command_event_sequence();
|
||||
|
||||
create or replace function device_assert_command_transition()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if old.lifecycle_state = new.lifecycle_state then
|
||||
return new;
|
||||
end if;
|
||||
if not (
|
||||
(old.lifecycle_state = 'draft' and new.lifecycle_state in ('planned', 'expired'))
|
||||
or (old.lifecycle_state = 'planned' and new.lifecycle_state in ('awaiting_confirmation', 'queued', 'expired'))
|
||||
or (old.lifecycle_state = 'awaiting_confirmation' and new.lifecycle_state in ('queued', 'expired'))
|
||||
or (old.lifecycle_state = 'queued' and new.lifecycle_state in ('dispatched', 'failed', 'expired'))
|
||||
or (old.lifecycle_state = 'dispatched' and new.lifecycle_state in ('acknowledged', 'failed', 'unknown'))
|
||||
or (old.lifecycle_state = 'acknowledged' and new.lifecycle_state in ('verified', 'failed', 'unknown'))
|
||||
) then
|
||||
raise check_violation using message = 'device_command_transition_invalid';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_commands_transition_guard on device_commands;
|
||||
|
||||
create trigger device_commands_transition_guard
|
||||
before update of lifecycle_state
|
||||
on device_commands
|
||||
for each row
|
||||
execute function device_assert_command_transition();
|
||||
|
||||
create or replace function device_assert_command_event_projection()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
current_state text;
|
||||
latest_event_state text;
|
||||
begin
|
||||
select dc.lifecycle_state into current_state
|
||||
from device_commands dc
|
||||
where dc.id = new.id;
|
||||
|
||||
select dce.to_state into latest_event_state
|
||||
from device_command_events dce
|
||||
where dce.command_id = new.id
|
||||
order by dce.sequence_number desc
|
||||
limit 1;
|
||||
|
||||
if current_state is null or latest_event_state is distinct from current_state then
|
||||
raise check_violation using message = 'device_command_event_projection_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_commands_event_projection_guard on device_commands;
|
||||
|
||||
create constraint trigger device_commands_event_projection_guard
|
||||
after insert or update of lifecycle_state
|
||||
on device_commands
|
||||
deferrable initially deferred
|
||||
for each row
|
||||
execute function device_assert_command_event_projection();
|
||||
|
||||
create or replace function device_assert_command_current_projection()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
current_state text;
|
||||
latest_event_state text;
|
||||
begin
|
||||
select dc.lifecycle_state into current_state
|
||||
from device_commands dc
|
||||
where dc.id = new.command_id;
|
||||
|
||||
select dce.to_state into latest_event_state
|
||||
from device_command_events dce
|
||||
where dce.command_id = new.command_id
|
||||
order by dce.sequence_number desc
|
||||
limit 1;
|
||||
|
||||
if current_state is null or latest_event_state is distinct from current_state then
|
||||
raise check_violation using message = 'device_command_current_projection_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_command_events_current_projection_guard
|
||||
on device_command_events;
|
||||
|
||||
create constraint trigger device_command_events_current_projection_guard
|
||||
after insert
|
||||
on device_command_events
|
||||
deferrable initially deferred
|
||||
for each row
|
||||
execute function device_assert_command_current_projection();
|
||||
|
||||
create or replace function device_reject_immutable_mutation()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
raise check_violation using message = 'device_immutable_record_mutation_forbidden';
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_configuration_revisions_immutable_guard
|
||||
on device_configuration_revisions;
|
||||
create trigger device_configuration_revisions_immutable_guard
|
||||
before update or delete or truncate
|
||||
on device_configuration_revisions
|
||||
for each statement
|
||||
execute function device_reject_immutable_mutation();
|
||||
|
||||
drop trigger if exists device_command_events_immutable_guard
|
||||
on device_command_events;
|
||||
create trigger device_command_events_immutable_guard
|
||||
before update or delete or truncate
|
||||
on device_command_events
|
||||
for each statement
|
||||
execute function device_reject_immutable_mutation();
|
||||
|
||||
drop trigger if exists device_audit_events_immutable_guard
|
||||
on device_audit_events;
|
||||
create trigger device_audit_events_immutable_guard
|
||||
before update or delete or truncate
|
||||
on device_audit_events
|
||||
for each statement
|
||||
execute function device_reject_immutable_mutation();
|
||||
|
||||
create or replace function device_require_control_resources_clear_before_transfer()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if exists (
|
||||
select 1 from device_resource_bindings drb
|
||||
where drb.device_id = old.id
|
||||
and drb.lifecycle_state in ('pending_external_approval', 'active')
|
||||
) then
|
||||
raise check_violation using message = 'device_transfer_active_resource_binding';
|
||||
end if;
|
||||
if exists (
|
||||
select 1 from device_configuration_state dcs
|
||||
where dcs.device_id = old.id
|
||||
and dcs.applied_revision_id is not null
|
||||
) then
|
||||
raise check_violation using message = 'device_transfer_applied_configuration';
|
||||
end if;
|
||||
if exists (
|
||||
select 1 from device_commands dc
|
||||
where dc.device_id = old.id
|
||||
and dc.lifecycle_state not in ('verified', 'failed', 'expired', 'unknown')
|
||||
) then
|
||||
raise check_violation using message = 'device_transfer_nonterminal_command';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_instances_control_resource_transfer_guard
|
||||
on device_instances;
|
||||
|
||||
create trigger device_instances_control_resource_transfer_guard
|
||||
before update of owner_scope_id, project_id
|
||||
on device_instances
|
||||
for each row
|
||||
when (
|
||||
old.owner_scope_id is distinct from new.owner_scope_id
|
||||
or old.project_id is distinct from new.project_id
|
||||
)
|
||||
execute function device_require_control_resources_clear_before_transfer();
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,31 @@
|
||||
begin;
|
||||
|
||||
alter table device_management_command_receipts
|
||||
drop constraint if exists device_management_command_receipts_command_kind_check;
|
||||
|
||||
alter table device_management_command_receipts
|
||||
add constraint device_management_command_receipts_command_kind_check
|
||||
check (command_kind in (
|
||||
'owner_scope.ensure',
|
||||
'project.ensure',
|
||||
'collection.ensure',
|
||||
'project_grant.upsert',
|
||||
'adapter_package.ensure',
|
||||
'adapter_version.register',
|
||||
'model_profile.register',
|
||||
'edge.ensure',
|
||||
'route.ensure',
|
||||
'enrollment_intent.ensure',
|
||||
'device.claim',
|
||||
'device.transfer',
|
||||
'discovery.reject',
|
||||
'discovery.expire',
|
||||
'device_credential_binding.upsert',
|
||||
'device_credential_binding.revoke',
|
||||
'device_binding.ensure',
|
||||
'device_binding.revoke',
|
||||
'device_configuration_revision.create',
|
||||
'device_configuration_desired.set'
|
||||
));
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,66 @@
|
||||
begin;
|
||||
|
||||
create table if not exists device_gateway_message_receipts (
|
||||
id uuid primary key,
|
||||
idempotency_key text not null
|
||||
check (idempotency_key ~ '^sha256:[a-f0-9]{64}$'),
|
||||
request_digest text not null
|
||||
check (request_digest ~ '^sha256:[a-f0-9]{64}$'),
|
||||
edge_ref text not null
|
||||
check (length(btrim(edge_ref)) between 3 and 128),
|
||||
adapter_ref text not null
|
||||
check (adapter_ref ~ '^[a-z][a-z0-9-]{1,62}$'),
|
||||
protocol_profile_ref text not null
|
||||
references device_model_profiles(profile_ref),
|
||||
protocol text not null
|
||||
check (protocol ~ '^[A-Z][A-Z0-9_]{0,31}$'),
|
||||
route_id uuid references device_routes(id),
|
||||
project_id uuid references device_projects(id),
|
||||
session_ref text not null
|
||||
check (length(btrim(session_ref)) between 3 and 128),
|
||||
message_ref text not null
|
||||
check (length(btrim(message_ref)) between 3 and 128),
|
||||
message_type text not null
|
||||
check (message_type ~ '^[a-z][a-z0-9._-]{1,127}$'),
|
||||
sequence bigint not null check (sequence > 0),
|
||||
identifier_kind text not null
|
||||
check (identifier_kind ~ '^[a-z][a-z0-9._:-]{1,63}$'),
|
||||
identifier_digest text not null
|
||||
check (identifier_digest ~ '^hmac-sha256:[a-f0-9]{64}$'),
|
||||
identifier_masked text not null
|
||||
check (length(identifier_masked) between 5 and 128),
|
||||
payload_schema_ref text not null
|
||||
check (length(btrim(payload_schema_ref)) between 3 and 128),
|
||||
payload jsonb not null,
|
||||
observed_at timestamptz not null,
|
||||
accepted_at timestamptz not null default now(),
|
||||
unique (idempotency_key),
|
||||
unique (edge_ref, session_ref, message_ref),
|
||||
foreign key (route_id, project_id)
|
||||
references device_routes(id, project_id),
|
||||
check (
|
||||
(route_id is null and project_id is null)
|
||||
or (route_id is not null and project_id is not null)
|
||||
)
|
||||
);
|
||||
|
||||
create index if not exists device_gateway_message_receipts_route_time_idx
|
||||
on device_gateway_message_receipts (route_id, accepted_at desc)
|
||||
where route_id is not null;
|
||||
|
||||
create index if not exists device_gateway_message_receipts_identity_time_idx
|
||||
on device_gateway_message_receipts (
|
||||
identifier_kind,
|
||||
identifier_digest,
|
||||
accepted_at desc
|
||||
);
|
||||
|
||||
drop trigger if exists device_gateway_message_receipts_immutable_guard
|
||||
on device_gateway_message_receipts;
|
||||
create trigger device_gateway_message_receipts_immutable_guard
|
||||
before update or delete or truncate
|
||||
on device_gateway_message_receipts
|
||||
for each statement
|
||||
execute function device_reject_immutable_mutation();
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,54 @@
|
||||
begin;
|
||||
|
||||
alter table device_edges
|
||||
add column if not exists channel_endpoint text,
|
||||
add column if not exists channel_servername text,
|
||||
add column if not exists channel_generation_ref text,
|
||||
add column if not exists channel_trust_bundle_ref text,
|
||||
add column if not exists channel_certificate_identities jsonb not null
|
||||
default '[]'::jsonb,
|
||||
add column if not exists channel_lifecycle_state text not null
|
||||
default 'disabled';
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_edges_channel_lifecycle_state_check'
|
||||
) then
|
||||
alter table device_edges add constraint device_edges_channel_lifecycle_state_check
|
||||
check (channel_lifecycle_state in ('disabled', 'active', 'revoked'));
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_edges_channel_configuration_check'
|
||||
) then
|
||||
alter table device_edges add constraint device_edges_channel_configuration_check
|
||||
check (
|
||||
(
|
||||
channel_lifecycle_state = 'disabled'
|
||||
and channel_endpoint is null
|
||||
and channel_servername is null
|
||||
and channel_generation_ref is null
|
||||
and channel_trust_bundle_ref is null
|
||||
and channel_certificate_identities = '[]'::jsonb
|
||||
)
|
||||
or
|
||||
(
|
||||
channel_lifecycle_state in ('active', 'revoked')
|
||||
and length(btrim(channel_endpoint)) between 12 and 256
|
||||
and channel_servername ~ '^[A-Za-z0-9.-]{1,253}$'
|
||||
and channel_generation_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
and channel_trust_bundle_ref ~ '^edge-trust:[a-z][a-z0-9-]{1,62}$'
|
||||
and jsonb_typeof(channel_certificate_identities) = 'array'
|
||||
and jsonb_array_length(channel_certificate_identities) between 1 and 2
|
||||
)
|
||||
);
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
create index if not exists device_edges_active_channel_idx
|
||||
on device_edges (channel_lifecycle_state, updated_at desc)
|
||||
where channel_lifecycle_state = 'active';
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,32 @@
|
||||
begin;
|
||||
|
||||
alter table device_management_command_receipts
|
||||
drop constraint if exists device_management_command_receipts_command_kind_check;
|
||||
|
||||
alter table device_management_command_receipts
|
||||
add constraint device_management_command_receipts_command_kind_check
|
||||
check (command_kind in (
|
||||
'owner_scope.ensure',
|
||||
'project.ensure',
|
||||
'collection.ensure',
|
||||
'project_grant.upsert',
|
||||
'adapter_package.ensure',
|
||||
'adapter_version.register',
|
||||
'model_profile.register',
|
||||
'edge.ensure',
|
||||
'route.ensure',
|
||||
'enrollment_intent.ensure',
|
||||
'device.claim',
|
||||
'device.update',
|
||||
'device.transfer',
|
||||
'discovery.reject',
|
||||
'discovery.expire',
|
||||
'device_credential_binding.upsert',
|
||||
'device_credential_binding.revoke',
|
||||
'device_binding.ensure',
|
||||
'device_binding.revoke',
|
||||
'device_configuration_revision.create',
|
||||
'device_configuration_desired.set'
|
||||
));
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,20 @@
|
||||
begin;
|
||||
|
||||
alter table device_instances
|
||||
add column if not exists integration_device_id text;
|
||||
|
||||
alter table device_instances
|
||||
drop constraint if exists device_instances_integration_device_id_check;
|
||||
|
||||
alter table device_instances
|
||||
add constraint device_instances_integration_device_id_check
|
||||
check (
|
||||
integration_device_id is null
|
||||
or (
|
||||
char_length(integration_device_id) between 1 and 160
|
||||
and integration_device_id = btrim(integration_device_id)
|
||||
and integration_device_id !~ '[[:cntrl:]]'
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
||||
Reference in New Issue
Block a user