Initial commit: AS4/411 directory and discovery service for Sankofa Marketplace
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
54
packages/storage/src/admin-port.ts
Normal file
54
packages/storage/src/admin-port.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type {
|
||||
Tenant,
|
||||
Participant,
|
||||
Identifier,
|
||||
Endpoint,
|
||||
Capability,
|
||||
CredentialRef,
|
||||
Policy,
|
||||
} from "@as4-411/core";
|
||||
import type { DirectoryStore } from "./port.js";
|
||||
|
||||
/**
|
||||
* Admin store: DirectoryStore read methods plus full CRUD for directory entities.
|
||||
* Used by Admin API; Postgres and InMemory can implement.
|
||||
*/
|
||||
export interface AdminStore extends DirectoryStore {
|
||||
// Tenants
|
||||
listTenants(): Promise<Tenant[]>;
|
||||
getTenant(id: string): Promise<Tenant | null>;
|
||||
createTenant(tenant: Tenant): Promise<void>;
|
||||
updateTenant(id: string, tenant: Omit<Tenant, "id">): Promise<boolean>;
|
||||
deleteTenant(id: string): Promise<boolean>;
|
||||
|
||||
// Participants
|
||||
listParticipants(options?: { tenantId?: string }): Promise<Participant[]>;
|
||||
getParticipant(id: string): Promise<Participant | null>;
|
||||
createParticipant(participant: Participant): Promise<void>;
|
||||
updateParticipant(id: string, participant: Omit<Participant, "id" | "tenantId">): Promise<boolean>;
|
||||
deleteParticipant(id: string): Promise<boolean>;
|
||||
|
||||
// Identifiers (create/list; list is getIdentifiersByParticipantId)
|
||||
createIdentifier(identifier: Identifier): Promise<void>;
|
||||
deleteIdentifier(id: string): Promise<boolean>;
|
||||
|
||||
// Endpoints
|
||||
createEndpoint(endpoint: Endpoint): Promise<void>;
|
||||
updateEndpoint(id: string, endpoint: Omit<Endpoint, "id" | "participantId">): Promise<boolean>;
|
||||
deleteEndpoint(id: string): Promise<boolean>;
|
||||
|
||||
// Capabilities
|
||||
createCapability(capability: Capability): Promise<void>;
|
||||
deleteCapability(id: string): Promise<boolean>;
|
||||
|
||||
// Credentials
|
||||
createCredential(credential: CredentialRef): Promise<void>;
|
||||
deleteCredential(id: string): Promise<boolean>;
|
||||
|
||||
// Policies
|
||||
listPolicies(options?: { tenantId?: string }): Promise<Policy[]>;
|
||||
getPolicy(id: string): Promise<Policy | null>;
|
||||
createPolicy(policy: Policy): Promise<void>;
|
||||
updatePolicy(id: string, policy: Omit<Policy, "id" | "tenantId">): Promise<boolean>;
|
||||
deletePolicy(id: string): Promise<boolean>;
|
||||
}
|
||||
8
packages/storage/src/index.ts
Normal file
8
packages/storage/src/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type { DirectoryStore } from "./port.js";
|
||||
export type { AdminStore } from "./admin-port.js";
|
||||
export type { RoutingArtifactStore } from "./routing-artifact-port.js";
|
||||
export { InMemoryDirectoryStore } from "./memory-store.js";
|
||||
export { InMemoryRoutingArtifactStore } from "./routing-artifact-memory.js";
|
||||
export { PostgresDirectoryStore } from "./postgres/postgres-store.js";
|
||||
export { PostgresRoutingArtifactStore } from "./postgres/routing-artifact-store.js";
|
||||
export type { PostgresStoreConfig } from "./postgres/postgres-store.js";
|
||||
242
packages/storage/src/memory-store.ts
Normal file
242
packages/storage/src/memory-store.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import type {
|
||||
Tenant,
|
||||
Participant,
|
||||
Identifier,
|
||||
Endpoint,
|
||||
Capability,
|
||||
CredentialRef,
|
||||
Policy,
|
||||
} from "@as4-411/core";
|
||||
import type { AdminStore } from "./admin-port.js";
|
||||
|
||||
/**
|
||||
* In-memory directory store for development and tests.
|
||||
* Implements AdminStore (read + write). Not persistent; no migrations.
|
||||
*/
|
||||
export class InMemoryDirectoryStore implements AdminStore {
|
||||
private tenants: Map<string, Tenant> = new Map();
|
||||
private participants: Map<string, Participant> = new Map();
|
||||
private identifiers: Identifier[] = [];
|
||||
private endpoints: Endpoint[] = [];
|
||||
private capabilities: Capability[] = [];
|
||||
private credentials: CredentialRef[] = [];
|
||||
private policies: Policy[] = [];
|
||||
|
||||
addParticipant(p: Participant): void {
|
||||
this.participants.set(p.id, p);
|
||||
}
|
||||
|
||||
addIdentifier(i: Identifier): void {
|
||||
this.identifiers.push(i);
|
||||
}
|
||||
|
||||
addEndpoint(e: Endpoint): void {
|
||||
this.endpoints.push(e);
|
||||
}
|
||||
|
||||
addCapability(c: Capability): void {
|
||||
this.capabilities.push(c);
|
||||
}
|
||||
|
||||
addCredential(c: CredentialRef): void {
|
||||
this.credentials.push(c);
|
||||
}
|
||||
|
||||
addPolicy(p: Policy): void {
|
||||
this.policies.push(p);
|
||||
}
|
||||
|
||||
addTenant(t: Tenant): void {
|
||||
this.tenants.set(t.id, t);
|
||||
}
|
||||
|
||||
async listTenants(): Promise<Tenant[]> {
|
||||
return Array.from(this.tenants.values());
|
||||
}
|
||||
|
||||
async getTenant(id: string): Promise<Tenant | null> {
|
||||
return this.tenants.get(id) ?? null;
|
||||
}
|
||||
|
||||
async createTenant(tenant: Tenant): Promise<void> {
|
||||
this.tenants.set(tenant.id, tenant);
|
||||
}
|
||||
|
||||
async updateTenant(id: string, tenant: Omit<Tenant, "id">): Promise<boolean> {
|
||||
const existing = this.tenants.get(id);
|
||||
if (!existing) return false;
|
||||
this.tenants.set(id, { ...existing, ...tenant });
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteTenant(id: string): Promise<boolean> {
|
||||
return this.tenants.delete(id);
|
||||
}
|
||||
|
||||
async listParticipants(options?: { tenantId?: string }): Promise<Participant[]> {
|
||||
let list = Array.from(this.participants.values());
|
||||
if (options?.tenantId)
|
||||
list = list.filter((p) => p.tenantId === options.tenantId);
|
||||
return list;
|
||||
}
|
||||
|
||||
async getParticipant(id: string): Promise<Participant | null> {
|
||||
return this.participants.get(id) ?? null;
|
||||
}
|
||||
|
||||
async createParticipant(participant: Participant): Promise<void> {
|
||||
this.participants.set(participant.id, participant);
|
||||
}
|
||||
|
||||
async updateParticipant(
|
||||
id: string,
|
||||
participant: Omit<Participant, "id" | "tenantId">
|
||||
): Promise<boolean> {
|
||||
const existing = this.participants.get(id);
|
||||
if (!existing) return false;
|
||||
this.participants.set(id, { ...existing, ...participant });
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteParticipant(id: string): Promise<boolean> {
|
||||
if (!this.participants.has(id)) return false;
|
||||
this.participants.delete(id);
|
||||
this.identifiers = this.identifiers.filter((i) => i.participantId !== id);
|
||||
this.endpoints = this.endpoints.filter((e) => e.participantId !== id);
|
||||
this.capabilities = this.capabilities.filter((c) => c.participantId !== id);
|
||||
this.credentials = this.credentials.filter((c) => c.participantId !== id);
|
||||
return true;
|
||||
}
|
||||
|
||||
async createIdentifier(identifier: Identifier): Promise<void> {
|
||||
this.identifiers.push(identifier);
|
||||
}
|
||||
|
||||
async deleteIdentifier(id: string): Promise<boolean> {
|
||||
const idx = this.identifiers.findIndex((i) => i.id === id);
|
||||
if (idx === -1) return false;
|
||||
this.identifiers.splice(idx, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
async createEndpoint(endpoint: Endpoint): Promise<void> {
|
||||
this.endpoints.push(endpoint);
|
||||
}
|
||||
|
||||
async updateEndpoint(
|
||||
id: string,
|
||||
endpoint: Omit<Endpoint, "id" | "participantId">
|
||||
): Promise<boolean> {
|
||||
const idx = this.endpoints.findIndex((e) => e.id === id);
|
||||
if (idx === -1) return false;
|
||||
this.endpoints[idx] = { ...this.endpoints[idx], ...endpoint };
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteEndpoint(id: string): Promise<boolean> {
|
||||
const idx = this.endpoints.findIndex((e) => e.id === id);
|
||||
if (idx === -1) return false;
|
||||
this.endpoints.splice(idx, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
async createCapability(capability: Capability): Promise<void> {
|
||||
this.capabilities.push(capability);
|
||||
}
|
||||
|
||||
async deleteCapability(id: string): Promise<boolean> {
|
||||
const idx = this.capabilities.findIndex((c) => c.id === id);
|
||||
if (idx === -1) return false;
|
||||
this.capabilities.splice(idx, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
async createCredential(credential: CredentialRef): Promise<void> {
|
||||
this.credentials.push(credential);
|
||||
}
|
||||
|
||||
async deleteCredential(id: string): Promise<boolean> {
|
||||
const idx = this.credentials.findIndex((c) => c.id === id);
|
||||
if (idx === -1) return false;
|
||||
this.credentials.splice(idx, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
async listPolicies(options?: { tenantId?: string }): Promise<Policy[]> {
|
||||
let list = Array.from(this.policies);
|
||||
if (options?.tenantId)
|
||||
list = list.filter((p) => p.tenantId === options.tenantId);
|
||||
return list.sort((a, b) => b.priority - a.priority);
|
||||
}
|
||||
|
||||
async getPolicy(id: string): Promise<Policy | null> {
|
||||
return this.policies.find((p) => p.id === id) ?? null;
|
||||
}
|
||||
|
||||
async createPolicy(policy: Policy): Promise<void> {
|
||||
this.policies.push(policy);
|
||||
}
|
||||
|
||||
async updatePolicy(id: string, policy: Omit<Policy, "id" | "tenantId">): Promise<boolean> {
|
||||
const idx = this.policies.findIndex((p) => p.id === id);
|
||||
if (idx === -1) return false;
|
||||
this.policies[idx] = { ...this.policies[idx], ...policy };
|
||||
return true;
|
||||
}
|
||||
|
||||
async deletePolicy(id: string): Promise<boolean> {
|
||||
const idx = this.policies.findIndex((p) => p.id === id);
|
||||
if (idx === -1) return false;
|
||||
this.policies.splice(idx, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
async findParticipantsByIdentifiers(
|
||||
identifiers: Array<{ type: string; value: string }>,
|
||||
options?: { tenantId?: string }
|
||||
): Promise<Participant[]> {
|
||||
const byKey = new Set<string>();
|
||||
for (const id of identifiers) {
|
||||
const matching = this.identifiers.filter(
|
||||
(i) =>
|
||||
i.identifier_type === id.type &&
|
||||
i.value === id.value &&
|
||||
(options?.tenantId == null ||
|
||||
this.participants.get(i.participantId)?.tenantId === options.tenantId)
|
||||
);
|
||||
for (const m of matching) byKey.add(m.participantId);
|
||||
}
|
||||
const out: Participant[] = [];
|
||||
for (const pid of byKey) {
|
||||
const p = this.participants.get(pid);
|
||||
if (p) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async getIdentifiersByParticipantId(participantId: string): Promise<Identifier[]> {
|
||||
return this.identifiers.filter((i) => i.participantId === participantId);
|
||||
}
|
||||
|
||||
async getEndpointsByParticipantId(
|
||||
participantId: string,
|
||||
options?: { protocol?: string; status?: string }
|
||||
): Promise<Endpoint[]> {
|
||||
let list = this.endpoints.filter((e) => e.participantId === participantId);
|
||||
if (options?.protocol) list = list.filter((e) => e.protocol === options.protocol);
|
||||
if (options?.status) list = list.filter((e) => e.status === options.status);
|
||||
return list;
|
||||
}
|
||||
|
||||
async getCapabilitiesByParticipantId(participantId: string): Promise<Capability[]> {
|
||||
return this.capabilities.filter((c) => c.participantId === participantId);
|
||||
}
|
||||
|
||||
async getCredentialsByParticipantId(participantId: string): Promise<CredentialRef[]> {
|
||||
return this.credentials.filter((c) => c.participantId === participantId);
|
||||
}
|
||||
|
||||
async getPoliciesByTenantId(tenantId: string): Promise<Policy[]> {
|
||||
return this.policies.filter((p) => p.tenantId === tenantId);
|
||||
}
|
||||
}
|
||||
38
packages/storage/src/port.ts
Normal file
38
packages/storage/src/port.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type {
|
||||
Participant,
|
||||
Identifier,
|
||||
Endpoint,
|
||||
Capability,
|
||||
CredentialRef,
|
||||
Policy,
|
||||
} from "@as4-411/core";
|
||||
|
||||
/**
|
||||
* Directory store port: enough for the resolver to find participants and endpoints.
|
||||
* Implementations: in-memory, Postgres, SQLite.
|
||||
*/
|
||||
export interface DirectoryStore {
|
||||
/** Find participants that have any of the given identifier (type, value) pairs, optionally scoped by tenant */
|
||||
findParticipantsByIdentifiers(
|
||||
identifiers: Array<{ type: string; value: string }>,
|
||||
options?: { tenantId?: string }
|
||||
): Promise<Participant[]>;
|
||||
|
||||
/** Get all identifiers for a participant */
|
||||
getIdentifiersByParticipantId(participantId: string): Promise<Identifier[]>;
|
||||
|
||||
/** Get all endpoints for a participant, optionally filter by protocol */
|
||||
getEndpointsByParticipantId(
|
||||
participantId: string,
|
||||
options?: { protocol?: string; status?: string }
|
||||
): Promise<Endpoint[]>;
|
||||
|
||||
/** Get capabilities for a participant */
|
||||
getCapabilitiesByParticipantId(participantId: string): Promise<Capability[]>;
|
||||
|
||||
/** Get credential refs for a participant */
|
||||
getCredentialsByParticipantId(participantId: string): Promise<CredentialRef[]>;
|
||||
|
||||
/** Get policies for a tenant (for policy filter step) */
|
||||
getPoliciesByTenantId(tenantId: string): Promise<Policy[]>;
|
||||
}
|
||||
388
packages/storage/src/postgres/postgres-store.ts
Normal file
388
packages/storage/src/postgres/postgres-store.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
import type { Pool } from "pg";
|
||||
import type {
|
||||
Tenant,
|
||||
Participant,
|
||||
Identifier,
|
||||
Endpoint,
|
||||
Capability,
|
||||
CredentialRef,
|
||||
Policy,
|
||||
} from "@as4-411/core";
|
||||
import type { AdminStore } from "../admin-port.js";
|
||||
|
||||
function rowToTenant(r: Record<string, unknown>): Tenant {
|
||||
return {
|
||||
id: r.id as string,
|
||||
name: r.name as string,
|
||||
createdAt: (r.created_at as Date)?.toISOString?.(),
|
||||
updatedAt: (r.updated_at as Date)?.toISOString?.(),
|
||||
};
|
||||
}
|
||||
|
||||
function rowToParticipant(r: Record<string, unknown>): Participant {
|
||||
return {
|
||||
id: r.id as string,
|
||||
tenantId: r.tenant_id as string,
|
||||
name: r.name as string,
|
||||
createdAt: (r.created_at as Date)?.toISOString?.(),
|
||||
updatedAt: (r.updated_at as Date)?.toISOString?.(),
|
||||
};
|
||||
}
|
||||
|
||||
function rowToIdentifier(r: Record<string, unknown>): Identifier {
|
||||
return {
|
||||
id: r.id as string,
|
||||
participantId: r.participant_id as string,
|
||||
identifier_type: r.identifier_type as string,
|
||||
value: r.value as string,
|
||||
scope: r.scope as string | undefined,
|
||||
priority: Number(r.priority) ?? 0,
|
||||
verified_at: (r.verified_at as Date)?.toISOString?.(),
|
||||
};
|
||||
}
|
||||
|
||||
function rowToEndpoint(r: Record<string, unknown>): Endpoint {
|
||||
return {
|
||||
id: r.id as string,
|
||||
participantId: r.participant_id as string,
|
||||
protocol: r.protocol as string,
|
||||
address: r.address as string,
|
||||
profile: r.profile as string | undefined,
|
||||
priority: Number(r.priority) ?? 0,
|
||||
status: r.status as "active" | "inactive" | "draining",
|
||||
};
|
||||
}
|
||||
|
||||
function rowToCapability(r: Record<string, unknown>): Capability {
|
||||
return {
|
||||
id: r.id as string,
|
||||
participantId: r.participant_id as string,
|
||||
service: r.service as string | undefined,
|
||||
action: r.action as string | undefined,
|
||||
process: r.process as string | undefined,
|
||||
document_type: r.document_type as string | undefined,
|
||||
constraints_json: r.constraints_json as Record<string, unknown> | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function rowToCredential(r: Record<string, unknown>): CredentialRef {
|
||||
return {
|
||||
id: r.id as string,
|
||||
participantId: r.participant_id as string,
|
||||
credential_type: r.credential_type as "tls" | "sign" | "encrypt",
|
||||
vault_ref: r.vault_ref as string,
|
||||
fingerprint: r.fingerprint as string | undefined,
|
||||
valid_from: (r.valid_from as Date)?.toISOString?.(),
|
||||
valid_to: (r.valid_to as Date)?.toISOString?.(),
|
||||
};
|
||||
}
|
||||
|
||||
function rowToPolicy(r: Record<string, unknown>): Policy {
|
||||
return {
|
||||
id: r.id as string,
|
||||
tenantId: r.tenant_id as string,
|
||||
rule_json: (r.rule_json as Record<string, unknown>) ?? {},
|
||||
effect: r.effect as "allow" | "deny",
|
||||
priority: Number(r.priority) ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export interface PostgresStoreConfig {
|
||||
pool: Pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Postgres implementation of AdminStore (read + write).
|
||||
* Run migrations (001_initial.sql) before use.
|
||||
*/
|
||||
export class PostgresDirectoryStore implements AdminStore {
|
||||
constructor(private readonly config: PostgresStoreConfig) {}
|
||||
|
||||
private get pool(): Pool {
|
||||
return this.config.pool;
|
||||
}
|
||||
|
||||
async findParticipantsByIdentifiers(
|
||||
identifiers: Array<{ type: string; value: string }>,
|
||||
options?: { tenantId?: string }
|
||||
): Promise<Participant[]> {
|
||||
if (identifiers.length === 0) return [];
|
||||
const values: unknown[] = identifiers.flatMap((id) => [id.type, id.value]);
|
||||
const conditions = identifiers
|
||||
.map((_, i) => `(i.identifier_type = $${2 * i + 1} AND i.value = $${2 * i + 2})`)
|
||||
.join(" OR ");
|
||||
let sql = `
|
||||
SELECT DISTINCT p.id, p.tenant_id, p.name, p.created_at, p.updated_at
|
||||
FROM participants p
|
||||
JOIN identifiers i ON i.participant_id = p.id
|
||||
WHERE ${conditions}
|
||||
`;
|
||||
if (options?.tenantId) {
|
||||
values.push(options.tenantId);
|
||||
sql += ` AND p.tenant_id = $${values.length}`;
|
||||
}
|
||||
const result = await this.pool.query(sql, values);
|
||||
return result.rows.map(rowToParticipant);
|
||||
}
|
||||
|
||||
async getIdentifiersByParticipantId(participantId: string): Promise<Identifier[]> {
|
||||
const result = await this.pool.query(
|
||||
"SELECT * FROM identifiers WHERE participant_id = $1",
|
||||
[participantId]
|
||||
);
|
||||
return result.rows.map(rowToIdentifier);
|
||||
}
|
||||
|
||||
async getEndpointsByParticipantId(
|
||||
participantId: string,
|
||||
options?: { protocol?: string; status?: string }
|
||||
): Promise<Endpoint[]> {
|
||||
let sql = "SELECT * FROM endpoints WHERE participant_id = $1";
|
||||
const values: unknown[] = [participantId];
|
||||
if (options?.protocol) {
|
||||
values.push(options.protocol);
|
||||
sql += ` AND protocol = $${values.length}`;
|
||||
}
|
||||
if (options?.status) {
|
||||
values.push(options.status);
|
||||
sql += ` AND status = $${values.length}`;
|
||||
}
|
||||
const result = await this.pool.query(sql, values);
|
||||
return result.rows.map(rowToEndpoint);
|
||||
}
|
||||
|
||||
async getCapabilitiesByParticipantId(participantId: string): Promise<Capability[]> {
|
||||
const result = await this.pool.query(
|
||||
"SELECT * FROM capabilities WHERE participant_id = $1",
|
||||
[participantId]
|
||||
);
|
||||
return result.rows.map(rowToCapability);
|
||||
}
|
||||
|
||||
async getCredentialsByParticipantId(participantId: string): Promise<CredentialRef[]> {
|
||||
const result = await this.pool.query(
|
||||
"SELECT * FROM credentials WHERE participant_id = $1",
|
||||
[participantId]
|
||||
);
|
||||
return result.rows.map(rowToCredential);
|
||||
}
|
||||
|
||||
async getPoliciesByTenantId(tenantId: string): Promise<Policy[]> {
|
||||
const result = await this.pool.query(
|
||||
"SELECT * FROM policies WHERE tenant_id = $1 ORDER BY priority DESC",
|
||||
[tenantId]
|
||||
);
|
||||
return result.rows.map(rowToPolicy);
|
||||
}
|
||||
|
||||
async listTenants(): Promise<Tenant[]> {
|
||||
const result = await this.pool.query("SELECT * FROM tenants ORDER BY id");
|
||||
return result.rows.map(rowToTenant);
|
||||
}
|
||||
|
||||
async getTenant(id: string): Promise<Tenant | null> {
|
||||
const result = await this.pool.query("SELECT * FROM tenants WHERE id = $1", [id]);
|
||||
return result.rows[0] ? rowToTenant(result.rows[0]) : null;
|
||||
}
|
||||
|
||||
async createTenant(tenant: Tenant): Promise<void> {
|
||||
await this.pool.query(
|
||||
"INSERT INTO tenants (id, name) VALUES ($1, $2)",
|
||||
[tenant.id, tenant.name]
|
||||
);
|
||||
}
|
||||
|
||||
async updateTenant(id: string, tenant: Omit<Tenant, "id">): Promise<boolean> {
|
||||
const result = await this.pool.query(
|
||||
"UPDATE tenants SET name = $1, updated_at = NOW() WHERE id = $2",
|
||||
[tenant.name, id]
|
||||
);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async deleteTenant(id: string): Promise<boolean> {
|
||||
const result = await this.pool.query("DELETE FROM tenants WHERE id = $1", [id]);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async listParticipants(options?: { tenantId?: string }): Promise<Participant[]> {
|
||||
let sql = "SELECT * FROM participants";
|
||||
const values: unknown[] = [];
|
||||
if (options?.tenantId) {
|
||||
values.push(options.tenantId);
|
||||
sql += " WHERE tenant_id = $1";
|
||||
}
|
||||
sql += " ORDER BY id";
|
||||
const result = await this.pool.query(sql, values);
|
||||
return result.rows.map(rowToParticipant);
|
||||
}
|
||||
|
||||
async getParticipant(id: string): Promise<Participant | null> {
|
||||
const result = await this.pool.query("SELECT * FROM participants WHERE id = $1", [id]);
|
||||
return result.rows[0] ? rowToParticipant(result.rows[0]) : null;
|
||||
}
|
||||
|
||||
async createParticipant(participant: Participant): Promise<void> {
|
||||
await this.pool.query(
|
||||
"INSERT INTO participants (id, tenant_id, name) VALUES ($1, $2, $3)",
|
||||
[participant.id, participant.tenantId, participant.name]
|
||||
);
|
||||
}
|
||||
|
||||
async updateParticipant(
|
||||
id: string,
|
||||
participant: Omit<Participant, "id" | "tenantId">
|
||||
): Promise<boolean> {
|
||||
const result = await this.pool.query(
|
||||
"UPDATE participants SET name = $1, updated_at = NOW() WHERE id = $2",
|
||||
[participant.name, id]
|
||||
);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async deleteParticipant(id: string): Promise<boolean> {
|
||||
const result = await this.pool.query("DELETE FROM participants WHERE id = $1", [id]);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async createIdentifier(identifier: Identifier): Promise<void> {
|
||||
await this.pool.query(
|
||||
"INSERT INTO identifiers (id, participant_id, identifier_type, value, scope, priority, verified_at) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
[
|
||||
identifier.id,
|
||||
identifier.participantId,
|
||||
identifier.identifier_type,
|
||||
identifier.value,
|
||||
identifier.scope ?? null,
|
||||
identifier.priority ?? 0,
|
||||
identifier.verified_at ? new Date(identifier.verified_at) : null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async deleteIdentifier(id: string): Promise<boolean> {
|
||||
const result = await this.pool.query("DELETE FROM identifiers WHERE id = $1", [id]);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async createEndpoint(endpoint: Endpoint): Promise<void> {
|
||||
await this.pool.query(
|
||||
"INSERT INTO endpoints (id, participant_id, protocol, address, profile, priority, status) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
[
|
||||
endpoint.id,
|
||||
endpoint.participantId,
|
||||
endpoint.protocol,
|
||||
endpoint.address,
|
||||
endpoint.profile ?? null,
|
||||
endpoint.priority ?? 0,
|
||||
endpoint.status,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async updateEndpoint(
|
||||
id: string,
|
||||
endpoint: Omit<Endpoint, "id" | "participantId">
|
||||
): Promise<boolean> {
|
||||
const result = await this.pool.query(
|
||||
"UPDATE endpoints SET protocol = $1, address = $2, profile = $3, priority = $4, status = $5 WHERE id = $6",
|
||||
[
|
||||
endpoint.protocol,
|
||||
endpoint.address,
|
||||
endpoint.profile ?? null,
|
||||
endpoint.priority ?? 0,
|
||||
endpoint.status,
|
||||
id,
|
||||
]
|
||||
);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async deleteEndpoint(id: string): Promise<boolean> {
|
||||
const result = await this.pool.query("DELETE FROM endpoints WHERE id = $1", [id]);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async createCapability(capability: Capability): Promise<void> {
|
||||
await this.pool.query(
|
||||
"INSERT INTO capabilities (id, participant_id, service, action, process, document_type, constraints_json) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
[
|
||||
capability.id,
|
||||
capability.participantId,
|
||||
capability.service ?? null,
|
||||
capability.action ?? null,
|
||||
capability.process ?? null,
|
||||
capability.document_type ?? null,
|
||||
capability.constraints_json ? JSON.stringify(capability.constraints_json) : null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async deleteCapability(id: string): Promise<boolean> {
|
||||
const result = await this.pool.query("DELETE FROM capabilities WHERE id = $1", [id]);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async createCredential(credential: CredentialRef): Promise<void> {
|
||||
await this.pool.query(
|
||||
"INSERT INTO credentials (id, participant_id, credential_type, vault_ref, fingerprint, valid_from, valid_to) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
[
|
||||
credential.id,
|
||||
credential.participantId,
|
||||
credential.credential_type,
|
||||
credential.vault_ref,
|
||||
credential.fingerprint ?? null,
|
||||
credential.valid_from ? new Date(credential.valid_from) : null,
|
||||
credential.valid_to ? new Date(credential.valid_to) : null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async deleteCredential(id: string): Promise<boolean> {
|
||||
const result = await this.pool.query("DELETE FROM credentials WHERE id = $1", [id]);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async listPolicies(options?: { tenantId?: string }): Promise<Policy[]> {
|
||||
let sql = "SELECT * FROM policies";
|
||||
const values: unknown[] = [];
|
||||
if (options?.tenantId) {
|
||||
values.push(options.tenantId);
|
||||
sql += " WHERE tenant_id = $1";
|
||||
}
|
||||
sql += " ORDER BY priority DESC, id";
|
||||
const result = await this.pool.query(sql, values);
|
||||
return result.rows.map(rowToPolicy);
|
||||
}
|
||||
|
||||
async getPolicy(id: string): Promise<Policy | null> {
|
||||
const result = await this.pool.query("SELECT * FROM policies WHERE id = $1", [id]);
|
||||
return result.rows[0] ? rowToPolicy(result.rows[0]) : null;
|
||||
}
|
||||
|
||||
async createPolicy(policy: Policy): Promise<void> {
|
||||
await this.pool.query(
|
||||
"INSERT INTO policies (id, tenant_id, rule_json, effect, priority) VALUES ($1, $2, $3, $4, $5)",
|
||||
[
|
||||
policy.id,
|
||||
policy.tenantId,
|
||||
JSON.stringify(policy.rule_json ?? {}),
|
||||
policy.effect,
|
||||
policy.priority ?? 0,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async updatePolicy(id: string, policy: Omit<Policy, "id" | "tenantId">): Promise<boolean> {
|
||||
const result = await this.pool.query(
|
||||
"UPDATE policies SET rule_json = $1, effect = $2, priority = $3 WHERE id = $4",
|
||||
[JSON.stringify(policy.rule_json ?? {}), policy.effect, policy.priority ?? 0, id]
|
||||
);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async deletePolicy(id: string): Promise<boolean> {
|
||||
const result = await this.pool.query("DELETE FROM policies WHERE id = $1", [id]);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
}
|
||||
79
packages/storage/src/postgres/routing-artifact-store.ts
Normal file
79
packages/storage/src/postgres/routing-artifact-store.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { Pool } from "pg";
|
||||
import type { RoutingArtifact } from "@as4-411/core";
|
||||
import type { RoutingArtifactStore } from "../routing-artifact-port.js";
|
||||
|
||||
function rowToArtifact(r: Record<string, unknown>): RoutingArtifact {
|
||||
return {
|
||||
id: r.id as string,
|
||||
tenantId: r.tenant_id as string | undefined,
|
||||
artifactType: r.artifact_type as RoutingArtifact["artifactType"],
|
||||
payload: r.artifact_payload as RoutingArtifact["payload"],
|
||||
effectiveFrom: (r.effective_from as Date)?.toISOString?.() ?? "",
|
||||
effectiveTo: (r.effective_to as Date)?.toISOString?.(),
|
||||
};
|
||||
}
|
||||
|
||||
export class PostgresRoutingArtifactStore implements RoutingArtifactStore {
|
||||
constructor(private readonly pool: Pool) {}
|
||||
|
||||
async get(
|
||||
artifactType: RoutingArtifact["artifactType"],
|
||||
options?: { tenantId?: string; atTime?: Date }
|
||||
): Promise<RoutingArtifact | null> {
|
||||
const at = options?.atTime ?? new Date();
|
||||
let sql =
|
||||
"SELECT * FROM routing_artifacts WHERE artifact_type = $1 AND effective_from <= $2 AND (effective_to IS NULL OR effective_to >= $2)";
|
||||
const values: unknown[] = [artifactType, at];
|
||||
if (options?.tenantId) {
|
||||
values.push(options.tenantId);
|
||||
sql += " AND tenant_id = $3";
|
||||
}
|
||||
sql += " ORDER BY effective_from DESC LIMIT 1";
|
||||
const result = await this.pool.query(sql, values);
|
||||
return result.rows[0] ? rowToArtifact(result.rows[0]) : null;
|
||||
}
|
||||
|
||||
async put(artifact: RoutingArtifact): Promise<void> {
|
||||
await this.pool.query(
|
||||
`INSERT INTO routing_artifacts (id, tenant_id, artifact_type, artifact_payload, effective_from, effective_to, signature, fingerprint)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
tenant_id = EXCLUDED.tenant_id,
|
||||
artifact_type = EXCLUDED.artifact_type,
|
||||
artifact_payload = EXCLUDED.artifact_payload,
|
||||
effective_from = EXCLUDED.effective_from,
|
||||
effective_to = EXCLUDED.effective_to,
|
||||
signature = EXCLUDED.signature,
|
||||
fingerprint = EXCLUDED.fingerprint`,
|
||||
[
|
||||
artifact.id,
|
||||
artifact.tenantId ?? null,
|
||||
artifact.artifactType,
|
||||
JSON.stringify(artifact.payload),
|
||||
new Date(artifact.effectiveFrom),
|
||||
artifact.effectiveTo ? new Date(artifact.effectiveTo) : null,
|
||||
artifact.payload.signature ?? null,
|
||||
artifact.payload.fingerprint ?? null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async list(
|
||||
options?: { tenantId?: string; artifactType?: RoutingArtifact["artifactType"] }
|
||||
): Promise<RoutingArtifact[]> {
|
||||
let sql = "SELECT * FROM routing_artifacts WHERE 1=1";
|
||||
const values: unknown[] = [];
|
||||
let n = 1;
|
||||
if (options?.tenantId) {
|
||||
values.push(options.tenantId);
|
||||
sql += ` AND tenant_id = $${n++}`;
|
||||
}
|
||||
if (options?.artifactType) {
|
||||
values.push(options.artifactType);
|
||||
sql += ` AND artifact_type = $${n++}`;
|
||||
}
|
||||
sql += " ORDER BY effective_from DESC";
|
||||
const result = await this.pool.query(sql, values);
|
||||
return result.rows.map(rowToArtifact);
|
||||
}
|
||||
}
|
||||
37
packages/storage/src/routing-artifact-memory.ts
Normal file
37
packages/storage/src/routing-artifact-memory.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { RoutingArtifact } from "@as4-411/core";
|
||||
import type { RoutingArtifactStore } from "./routing-artifact-port.js";
|
||||
|
||||
export class InMemoryRoutingArtifactStore implements RoutingArtifactStore {
|
||||
private artifacts: RoutingArtifact[] = [];
|
||||
|
||||
async get(
|
||||
artifactType: RoutingArtifact["artifactType"],
|
||||
options?: { tenantId?: string; atTime?: Date }
|
||||
): Promise<RoutingArtifact | null> {
|
||||
const at = options?.atTime ?? new Date();
|
||||
const list = this.artifacts.filter(
|
||||
(a) =>
|
||||
a.artifactType === artifactType &&
|
||||
(options?.tenantId == null || a.tenantId === options.tenantId) &&
|
||||
new Date(a.effectiveFrom) <= at &&
|
||||
(a.effectiveTo == null || new Date(a.effectiveTo) >= at)
|
||||
);
|
||||
list.sort((a, b) => new Date(b.effectiveFrom).getTime() - new Date(a.effectiveFrom).getTime());
|
||||
return list[0] ?? null;
|
||||
}
|
||||
|
||||
async put(artifact: RoutingArtifact): Promise<void> {
|
||||
const idx = this.artifacts.findIndex((a) => a.id === artifact.id);
|
||||
if (idx >= 0) this.artifacts[idx] = artifact;
|
||||
else this.artifacts.push(artifact);
|
||||
}
|
||||
|
||||
async list(
|
||||
options?: { tenantId?: string; artifactType?: RoutingArtifact["artifactType"] }
|
||||
): Promise<RoutingArtifact[]> {
|
||||
let list = [...this.artifacts];
|
||||
if (options?.tenantId) list = list.filter((a) => a.tenantId === options.tenantId);
|
||||
if (options?.artifactType) list = list.filter((a) => a.artifactType === options.artifactType);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
18
packages/storage/src/routing-artifact-port.ts
Normal file
18
packages/storage/src/routing-artifact-port.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { RoutingArtifact } from "@as4-411/core";
|
||||
|
||||
/**
|
||||
* Store for routing artifacts (BIN table, GTT table, participant_map, fallback_rules).
|
||||
* Optional: resolver can use this when resolving by BIN or GTT.
|
||||
*/
|
||||
export interface RoutingArtifactStore {
|
||||
get(
|
||||
artifactType: RoutingArtifact["artifactType"],
|
||||
options?: { tenantId?: string; atTime?: Date }
|
||||
): Promise<RoutingArtifact | null>;
|
||||
|
||||
put(artifact: RoutingArtifact): Promise<void>;
|
||||
|
||||
list(
|
||||
options?: { tenantId?: string; artifactType?: RoutingArtifact["artifactType"] }
|
||||
): Promise<RoutingArtifact[]>;
|
||||
}
|
||||
Reference in New Issue
Block a user