feat(api): absorb the Hub control plane into the local instance model
Add central notification channels, rules, queue and delivery logs, fleet organization groups and tags, device discovery, the device action catalog, instance module reads, the log centre, connection settings and system maintenance as native /api/v1 routes backed by the existing secret store, audit trail and pinned upstream transport.
This commit is contained in:
@@ -53,6 +53,7 @@ interface InstanceRow {
|
||||
base_url: string;
|
||||
config_revision: number;
|
||||
updated_at: string;
|
||||
group_id: string | null;
|
||||
}
|
||||
interface SecretRow {
|
||||
id: string;
|
||||
@@ -161,6 +162,7 @@ export class InstanceService {
|
||||
const name = normalizeName(input.name);
|
||||
const origin = normalizeOrigin(input.origin);
|
||||
const tags = normalizeTags(input.tags);
|
||||
const groupId = this.normalizeGroupId(input.groupId);
|
||||
validatePassword(input.password);
|
||||
const instanceId = this.id();
|
||||
let newSecret: { id: string; external: string } | undefined;
|
||||
@@ -171,9 +173,9 @@ export class InstanceService {
|
||||
this.db.transaction(() => {
|
||||
this.db
|
||||
.prepare(
|
||||
'INSERT INTO instances (id,name,base_url,auth_mode,enabled,config_revision,created_at,updated_at) VALUES (?,?,?, ?,1,1,?,?)',
|
||||
'INSERT INTO instances (id,name,base_url,auth_mode,enabled,config_revision,created_at,updated_at,group_id) VALUES (?,?,?, ?,1,1,?,?,?)',
|
||||
)
|
||||
.run(instanceId, name, origin, newSecret ? 'password' : 'none', now, now);
|
||||
.run(instanceId, name, origin, newSecret ? 'password' : 'none', now, now, groupId);
|
||||
this.replaceTags(instanceId, tags, now);
|
||||
if (newSecret) this.insertReference(newSecret.id, instanceId, newSecret.external, now);
|
||||
})();
|
||||
@@ -217,6 +219,8 @@ export class InstanceService {
|
||||
);
|
||||
if (query.tag !== undefined)
|
||||
values = values.filter(({ value }) => value.tags.includes(query.tag!.trim()));
|
||||
if (query.groupId !== undefined)
|
||||
values = values.filter(({ value }) => value.groupId === query.groupId!.trim());
|
||||
if (query.credentialConfigured !== undefined)
|
||||
values = values.filter(
|
||||
({ value }) => value.credentialConfigured === query.credentialConfigured,
|
||||
@@ -257,6 +261,8 @@ export class InstanceService {
|
||||
const name = patch.name === undefined ? current.name : normalizeName(patch.name);
|
||||
const origin = patch.origin === undefined ? current.base_url : normalizeOrigin(patch.origin);
|
||||
const tags = patch.tags === undefined ? undefined : normalizeTags(patch.tags);
|
||||
const groupId =
|
||||
patch.groupId === undefined ? current.group_id : this.normalizeGroupId(patch.groupId);
|
||||
let committedOldSecret: SecretRow | undefined;
|
||||
let newSecret: { id: string; external: string } | undefined;
|
||||
if (patch.password?.action === 'set')
|
||||
@@ -272,7 +278,7 @@ export class InstanceService {
|
||||
const transactionOldSecret = this.secret(instanceId);
|
||||
const changed = this.db
|
||||
.prepare(
|
||||
'UPDATE instances SET name=?,base_url=?,auth_mode=?,config_revision=config_revision+1,updated_at=? WHERE id=? AND config_revision=?',
|
||||
'UPDATE instances SET name=?,base_url=?,auth_mode=?,config_revision=config_revision+1,updated_at=?,group_id=? WHERE id=? AND config_revision=?',
|
||||
)
|
||||
.run(
|
||||
name,
|
||||
@@ -281,6 +287,7 @@ export class InstanceService {
|
||||
? 'password'
|
||||
: 'none',
|
||||
now,
|
||||
groupId,
|
||||
instanceId,
|
||||
revision,
|
||||
);
|
||||
@@ -542,13 +549,23 @@ export class InstanceService {
|
||||
private loadRows(where: string, parameters: unknown[]): AggregateRow[] {
|
||||
return this.db
|
||||
.prepare(
|
||||
`SELECT i.id,i.name,i.base_url,i.config_revision,i.updated_at,
|
||||
`SELECT i.id,i.name,i.base_url,i.config_revision,i.updated_at,i.group_id,
|
||||
group_concat(t.tag, char(31)) tags,
|
||||
CASE WHEN EXISTS(SELECT 1 FROM secret_references r WHERE r.instance_id=i.id AND r.purpose=?) THEN 1 ELSE 0 END credential
|
||||
FROM instances i LEFT JOIN instance_tags t ON t.instance_id=i.id ${where} GROUP BY i.id`,
|
||||
)
|
||||
.all(PURPOSE, ...parameters) as AggregateRow[];
|
||||
}
|
||||
|
||||
private normalizeGroupId(value: string | null | undefined): string | null {
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value !== 'string') validation('groupId must be a string or null');
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
const exists = this.db.prepare('SELECT 1 FROM device_groups WHERE id=?').get(trimmed);
|
||||
if (!exists) throw new InstanceServiceError('NOT_FOUND', 'Device group was not found');
|
||||
return trimmed;
|
||||
}
|
||||
private toInstance(row: AggregateRow): Instance {
|
||||
const capabilityRows = this.db
|
||||
.prepare('SELECT state FROM capabilities WHERE instance_id=?')
|
||||
@@ -577,6 +594,7 @@ export class InstanceService {
|
||||
name: row.name,
|
||||
origin: row.base_url,
|
||||
tags: row.tags ? row.tags.split(String.fromCharCode(31)).sort(codePointCompare) : [],
|
||||
groupId: row.group_id ?? null,
|
||||
revision: row.config_revision,
|
||||
capabilityStatus,
|
||||
freshness,
|
||||
|
||||
Reference in New Issue
Block a user