OPC # 0001: Extract OPC into standalone repo
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
using ControlPlane.Core.Models;
|
||||
using Npgsql;
|
||||
|
||||
namespace ControlPlane.Api.Services;
|
||||
|
||||
public class OpcService(NpgsqlDataSource db)
|
||||
{
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private static OpcRecord ReadOpc(NpgsqlDataReader r) => new(
|
||||
r.GetGuid(0),
|
||||
r.GetString(1),
|
||||
r.GetString(2),
|
||||
r.GetString(3),
|
||||
r.GetString(4),
|
||||
r.GetString(5),
|
||||
r.GetString(6),
|
||||
r.GetString(7),
|
||||
r.GetDateTime(8),
|
||||
r.GetDateTime(9)
|
||||
);
|
||||
|
||||
private static OpcNote ReadNote(NpgsqlDataReader r) => new(
|
||||
r.GetGuid(0),
|
||||
r.GetGuid(1),
|
||||
r.GetString(2),
|
||||
r.GetString(3),
|
||||
r.GetDateTime(4)
|
||||
);
|
||||
|
||||
private static OpcArtifact ReadArtifact(NpgsqlDataReader r) => new(
|
||||
r.GetGuid(0),
|
||||
r.GetGuid(1),
|
||||
r.GetString(2),
|
||||
r.GetString(3),
|
||||
r.GetString(4),
|
||||
r.GetDateTime(5),
|
||||
r.GetDateTime(6)
|
||||
);
|
||||
|
||||
// ── Next OPC number ───────────────────────────────────────────────────────
|
||||
|
||||
public async Task<string> NextNumberAsync(CancellationToken ct = default)
|
||||
{
|
||||
await using var cmd = db.CreateCommand(
|
||||
"SELECT number FROM opc ORDER BY CAST(TRIM(SUBSTRING(number FROM 7)) AS INTEGER) DESC LIMIT 1");
|
||||
var last = await cmd.ExecuteScalarAsync(ct) as string;
|
||||
if (last is null) return "OPC # 0001";
|
||||
if (int.TryParse(last[6..], out var n))
|
||||
return $"OPC # {n + 1:D4}";
|
||||
return "OPC # 0001";
|
||||
}
|
||||
|
||||
// ── OPC CRUD ──────────────────────────────────────────────────────────────
|
||||
|
||||
public async Task<List<OpcRecord>> ListAsync(
|
||||
string? typeFilter = null, string? statusFilter = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var sql = """
|
||||
SELECT id, number, title, description, type, status, priority, assignee,
|
||||
created_at, updated_at
|
||||
FROM opc
|
||||
WHERE ($1::text IS NULL OR type = $1)
|
||||
AND ($2::text IS NULL OR status = $2)
|
||||
ORDER BY created_at DESC
|
||||
""";
|
||||
await using var cmd = db.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue(typeFilter ?? (object)DBNull.Value);
|
||||
cmd.Parameters.AddWithValue(statusFilter ?? (object)DBNull.Value);
|
||||
await using var r = await cmd.ExecuteReaderAsync(ct);
|
||||
var list = new List<OpcRecord>();
|
||||
while (await r.ReadAsync(ct)) list.Add(ReadOpc(r));
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<OpcRecord?> GetAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
await using var cmd = db.CreateCommand(
|
||||
"SELECT id, number, title, description, type, status, priority, assignee, created_at, updated_at FROM opc WHERE id = $1");
|
||||
cmd.Parameters.AddWithValue(id);
|
||||
await using var r = await cmd.ExecuteReaderAsync(ct);
|
||||
return await r.ReadAsync(ct) ? ReadOpc(r) : null;
|
||||
}
|
||||
|
||||
public async Task<OpcRecord> CreateAsync(CreateOpcRequest req, CancellationToken ct = default)
|
||||
{
|
||||
var number = await NextNumberAsync(ct);
|
||||
var sql = """
|
||||
INSERT INTO opc (number, title, description, type, status, priority, assignee)
|
||||
VALUES ($1, $2, $3, $4, 'New', $5, $6)
|
||||
RETURNING id, number, title, description, type, status, priority, assignee,
|
||||
created_at, updated_at
|
||||
""";
|
||||
await using var cmd = db.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue(number);
|
||||
cmd.Parameters.AddWithValue(req.Title);
|
||||
cmd.Parameters.AddWithValue(req.Description);
|
||||
cmd.Parameters.AddWithValue(req.Type);
|
||||
cmd.Parameters.AddWithValue(req.Priority);
|
||||
cmd.Parameters.AddWithValue(req.Assignee);
|
||||
await using var r = await cmd.ExecuteReaderAsync(ct);
|
||||
await r.ReadAsync(ct);
|
||||
return ReadOpc(r);
|
||||
}
|
||||
|
||||
public async Task<OpcRecord?> UpdateAsync(Guid id, UpdateOpcRequest req, CancellationToken ct = default)
|
||||
{
|
||||
var sql = """
|
||||
UPDATE opc SET
|
||||
title = COALESCE($2, title),
|
||||
description = COALESCE($3, description),
|
||||
type = COALESCE($4, type),
|
||||
status = COALESCE($5, status),
|
||||
priority = COALESCE($6, priority),
|
||||
assignee = COALESCE($7, assignee),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING id, number, title, description, type, status, priority, assignee,
|
||||
created_at, updated_at
|
||||
""";
|
||||
await using var cmd = db.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue(id);
|
||||
cmd.Parameters.AddWithValue(req.Title ?? (object)DBNull.Value);
|
||||
cmd.Parameters.AddWithValue(req.Description ?? (object)DBNull.Value);
|
||||
cmd.Parameters.AddWithValue(req.Type ?? (object)DBNull.Value);
|
||||
cmd.Parameters.AddWithValue(req.Status ?? (object)DBNull.Value);
|
||||
cmd.Parameters.AddWithValue(req.Priority ?? (object)DBNull.Value);
|
||||
cmd.Parameters.AddWithValue(req.Assignee ?? (object)DBNull.Value);
|
||||
await using var r = await cmd.ExecuteReaderAsync(ct);
|
||||
return await r.ReadAsync(ct) ? ReadOpc(r) : null;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
await using var cmd = db.CreateCommand("DELETE FROM opc WHERE id = $1");
|
||||
cmd.Parameters.AddWithValue(id);
|
||||
return await cmd.ExecuteNonQueryAsync(ct) > 0;
|
||||
}
|
||||
|
||||
// ── Notes ──────────────────────────────────────────────────────────────────
|
||||
|
||||
public async Task<List<OpcNote>> ListNotesAsync(Guid opcId, CancellationToken ct = default)
|
||||
{
|
||||
await using var cmd = db.CreateCommand(
|
||||
"SELECT id, opc_id, author, content, created_at FROM opc_note WHERE opc_id = $1 ORDER BY created_at ASC");
|
||||
cmd.Parameters.AddWithValue(opcId);
|
||||
await using var r = await cmd.ExecuteReaderAsync(ct);
|
||||
var list = new List<OpcNote>();
|
||||
while (await r.ReadAsync(ct)) list.Add(ReadNote(r));
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<OpcNote> AddNoteAsync(Guid opcId, AddNoteRequest req, CancellationToken ct = default)
|
||||
{
|
||||
var sql = """
|
||||
INSERT INTO opc_note (opc_id, author, content)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, opc_id, author, content, created_at
|
||||
""";
|
||||
await using var cmd = db.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue(opcId);
|
||||
cmd.Parameters.AddWithValue(req.Author);
|
||||
cmd.Parameters.AddWithValue(req.Content);
|
||||
await using var r = await cmd.ExecuteReaderAsync(ct);
|
||||
await r.ReadAsync(ct);
|
||||
return ReadNote(r);
|
||||
}
|
||||
|
||||
// ── Artifacts ─────────────────────────────────────────────────────────────
|
||||
|
||||
public async Task<List<OpcArtifact>> ListArtifactsAsync(Guid opcId, string? artifactType = null, CancellationToken ct = default)
|
||||
{
|
||||
var sql = """
|
||||
SELECT id, opc_id, artifact_type, title, content, created_at, updated_at
|
||||
FROM opc_artifact
|
||||
WHERE opc_id = $1
|
||||
AND ($2::text IS NULL OR artifact_type = $2)
|
||||
ORDER BY created_at ASC
|
||||
""";
|
||||
await using var cmd = db.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue(opcId);
|
||||
cmd.Parameters.AddWithValue(artifactType ?? (object)DBNull.Value);
|
||||
await using var r = await cmd.ExecuteReaderAsync(ct);
|
||||
var list = new List<OpcArtifact>();
|
||||
while (await r.ReadAsync(ct)) list.Add(ReadArtifact(r));
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<OpcArtifact> UpsertArtifactAsync(Guid opcId, UpsertArtifactRequest req, CancellationToken ct = default)
|
||||
{
|
||||
var sql = """
|
||||
INSERT INTO opc_artifact (opc_id, artifact_type, title, content)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id, opc_id, artifact_type, title, content, created_at, updated_at
|
||||
""";
|
||||
// Simple insert; for updates use artifact id endpoint
|
||||
await using var cmd = db.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue(opcId);
|
||||
cmd.Parameters.AddWithValue(req.ArtifactType);
|
||||
cmd.Parameters.AddWithValue(req.Title);
|
||||
cmd.Parameters.AddWithValue(req.Content);
|
||||
await using var r = await cmd.ExecuteReaderAsync(ct);
|
||||
await r.ReadAsync(ct);
|
||||
return ReadArtifact(r);
|
||||
}
|
||||
|
||||
public async Task<OpcArtifact?> UpdateArtifactAsync(Guid artifactId, UpsertArtifactRequest req, CancellationToken ct = default)
|
||||
{
|
||||
var sql = """
|
||||
UPDATE opc_artifact SET
|
||||
title = $2,
|
||||
content = $3,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING id, opc_id, artifact_type, title, content, created_at, updated_at
|
||||
""";
|
||||
await using var cmd = db.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue(artifactId);
|
||||
cmd.Parameters.AddWithValue(req.Title);
|
||||
cmd.Parameters.AddWithValue(req.Content);
|
||||
await using var r = await cmd.ExecuteReaderAsync(ct);
|
||||
return await r.ReadAsync(ct) ? ReadArtifact(r) : null;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteArtifactAsync(Guid artifactId, CancellationToken ct = default)
|
||||
{
|
||||
await using var cmd = db.CreateCommand("DELETE FROM opc_artifact WHERE id = $1");
|
||||
cmd.Parameters.AddWithValue(artifactId);
|
||||
return await cmd.ExecuteNonQueryAsync(ct) > 0;
|
||||
}
|
||||
|
||||
// ── Pinned commits ────────────────────────────────────────────────────────
|
||||
|
||||
private static OpcPinnedCommit ReadPinnedCommit(NpgsqlDataReader r) => new(
|
||||
r.GetGuid(0),
|
||||
r.GetString(1),
|
||||
r.GetString(2),
|
||||
r.GetString(3),
|
||||
r.GetString(4),
|
||||
r.GetDateTime(5),
|
||||
r.GetString(6)
|
||||
);
|
||||
|
||||
public async Task<List<OpcPinnedCommit>> ListPinnedCommitsAsync(Guid opcId, CancellationToken ct = default)
|
||||
{
|
||||
await using var cmd = db.CreateCommand(
|
||||
"SELECT opc_id, hash, short_hash, subject, author, pinned_at, pinned_by FROM opc_pinned_commit WHERE opc_id = $1 ORDER BY pinned_at DESC");
|
||||
cmd.Parameters.AddWithValue(opcId);
|
||||
await using var r = await cmd.ExecuteReaderAsync(ct);
|
||||
var list = new List<OpcPinnedCommit>();
|
||||
while (await r.ReadAsync(ct)) list.Add(ReadPinnedCommit(r));
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<OpcPinnedCommit?> PinCommitAsync(
|
||||
Guid opcId, string hash, string shortHash, string subject, string author, string pinnedBy,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Verify the OPC exists
|
||||
await using var existsCmd = db.CreateCommand("SELECT 1 FROM opc WHERE id = $1");
|
||||
existsCmd.Parameters.AddWithValue(opcId);
|
||||
var exists = await existsCmd.ExecuteScalarAsync(ct);
|
||||
if (exists is null) return null;
|
||||
|
||||
var sql = """
|
||||
INSERT INTO opc_pinned_commit (opc_id, hash, short_hash, subject, author, pinned_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (opc_id, hash) DO UPDATE SET
|
||||
short_hash = EXCLUDED.short_hash,
|
||||
subject = EXCLUDED.subject,
|
||||
author = EXCLUDED.author,
|
||||
pinned_by = EXCLUDED.pinned_by,
|
||||
pinned_at = NOW()
|
||||
RETURNING opc_id, hash, short_hash, subject, author, pinned_at, pinned_by
|
||||
""";
|
||||
await using var cmd = db.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue(opcId);
|
||||
cmd.Parameters.AddWithValue(hash);
|
||||
cmd.Parameters.AddWithValue(shortHash);
|
||||
cmd.Parameters.AddWithValue(subject);
|
||||
cmd.Parameters.AddWithValue(author);
|
||||
cmd.Parameters.AddWithValue(pinnedBy);
|
||||
await using var r = await cmd.ExecuteReaderAsync(ct);
|
||||
return await r.ReadAsync(ct) ? ReadPinnedCommit(r) : null;
|
||||
}
|
||||
|
||||
public async Task<bool> UnpinCommitAsync(Guid opcId, string hash, CancellationToken ct = default)
|
||||
{
|
||||
await using var cmd = db.CreateCommand(
|
||||
"DELETE FROM opc_pinned_commit WHERE opc_id = $1 AND hash = $2");
|
||||
cmd.Parameters.AddWithValue(opcId);
|
||||
cmd.Parameters.AddWithValue(hash);
|
||||
return await cmd.ExecuteNonQueryAsync(ct) > 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user