feat: add cloud api skeleton

This commit is contained in:
HWLAB Code Queue
2026-05-21 14:39:30 +00:00
parent 8f3069035f
commit b1d31380cb
10 changed files with 1004 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env node
import { createCloudApiServer } from "../../internal/cloud/server.mjs";
const host = process.env.HWLAB_CLOUD_API_HOST || "0.0.0.0";
const port = Number.parseInt(process.env.HWLAB_CLOUD_API_PORT || process.env.PORT || "6667", 10);
const server = createCloudApiServer();
server.listen(port, host, () => {
const address = server.address();
const resolvedPort = typeof address === "object" && address ? address.port : port;
process.stdout.write(
`${JSON.stringify({
serviceId: "hwlab-cloud-api",
status: "listening",
host,
port: resolvedPort
})}\n`
);
});
for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => {
server.close(() => {
process.exit(0);
});
});
}
+145
View File
@@ -0,0 +1,145 @@
import { ENVIRONMENT_DEV } from "../protocol/index.mjs";
export const CLOUD_API_SERVICE_ID = "hwlab-cloud-api";
export const AUDIT_FIELD_NAMES = Object.freeze([
"requestId",
"actor",
"source",
"operation",
"target",
"result",
"timestamp"
]);
export function createAuditRecord(input = {}) {
const record = {
requestId: asString(input.requestId, "req_unassigned"),
actor: normalizeActor(input.actor),
source: normalizeSource(input.source, input.traceId),
operation: asString(input.operation, "cloud_api.request"),
target: normalizeTarget(input.target),
result: asString(input.result, "unknown"),
timestamp: asString(input.timestamp, new Date().toISOString())
};
assertAuditFields(record);
return record;
}
export function deriveActorFromMeta(meta = {}) {
return {
type: meta.actorType || "system",
id: meta.actorId || `system_${CLOUD_API_SERVICE_ID}`
};
}
export function assertAuditFields(record) {
if (!record || typeof record !== "object" || Array.isArray(record)) {
throw new Error("audit record must be an object");
}
for (const field of AUDIT_FIELD_NAMES) {
if (!Object.hasOwn(record, field)) {
throw new Error(`audit.${field} is required`);
}
}
if (Number.isNaN(Date.parse(record.timestamp))) {
throw new Error("audit.timestamp must be an RFC 3339 timestamp");
}
}
function normalizeActor(actor) {
if (!actor) {
return {
type: "system",
id: `system_${CLOUD_API_SERVICE_ID}`
};
}
if (typeof actor === "string") {
return {
type: "user",
id: actor
};
}
if (typeof actor === "object" && !Array.isArray(actor)) {
return {
type: asString(actor.type, "user"),
id: asString(actor.id, `system_${CLOUD_API_SERVICE_ID}`)
};
}
return {
type: "system",
id: `system_${CLOUD_API_SERVICE_ID}`
};
}
function normalizeSource(source, traceId) {
const base = {
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV
};
if (traceId) {
base.traceId = traceId;
}
if (!source) {
return base;
}
if (typeof source === "string") {
return {
...base,
serviceId: source
};
}
if (typeof source === "object" && !Array.isArray(source)) {
return {
...base,
...source
};
}
return base;
}
function normalizeTarget(target) {
if (!target) {
return {
type: "cloud_api",
id: CLOUD_API_SERVICE_ID
};
}
if (typeof target === "string") {
return {
type: "resource",
id: target
};
}
if (typeof target === "object" && !Array.isArray(target)) {
return {
type: asString(target.type, "resource"),
id: asString(target.id, "target_unspecified")
};
}
return {
type: "cloud_api",
id: CLOUD_API_SERVICE_ID
};
}
function asString(value, fallback) {
if (value === undefined || value === null || value === "") {
return fallback;
}
return String(value);
}
+51
View File
@@ -0,0 +1,51 @@
import assert from "node:assert/strict";
import test from "node:test";
import { AUDIT_FIELD_NAMES, assertAuditFields, createAuditRecord } from "./index.mjs";
test("createAuditRecord exposes the L1 audit field names", () => {
const record = createAuditRecord({
requestId: "req_01J00000000000000000000000",
actor: {
type: "user",
id: "usr_01J00000000000000000000000"
},
source: {
serviceId: "hwlab-cloud-api",
adapter: "json-rpc"
},
operation: "hardware.operation.request",
target: {
type: "project",
id: "prj_01J00000000000000000000000"
},
result: "rejected",
timestamp: "2026-05-21T00:00:00.000Z"
});
for (const field of AUDIT_FIELD_NAMES) {
assert.ok(Object.hasOwn(record, field), `expected audit.${field}`);
}
assert.equal(record.requestId, "req_01J00000000000000000000000");
assert.equal(record.actor.id, "usr_01J00000000000000000000000");
assert.equal(record.source.serviceId, "hwlab-cloud-api");
assert.equal(record.operation, "hardware.operation.request");
assert.equal(record.target.type, "project");
assert.equal(record.result, "rejected");
});
test("assertAuditFields rejects missing audit fields", () => {
assert.throws(
() =>
assertAuditFields({
requestId: "req_01J00000000000000000000000",
actor: {},
source: {},
operation: "hardware.operation.request",
target: {},
result: "rejected"
}),
/audit.timestamp is required/
);
});
+243
View File
@@ -0,0 +1,243 @@
import { randomUUID } from "node:crypto";
import {
ENVIRONMENT_DEV,
ERROR_CODES,
JSON_RPC_VERSION,
validateRequest,
validateResponse
} from "../protocol/index.mjs";
import {
AUDIT_FIELD_NAMES,
CLOUD_API_SERVICE_ID,
createAuditRecord,
deriveActorFromMeta
} from "../audit/index.mjs";
export const SUPPORTED_RPC_METHODS = Object.freeze([
"system.health",
"cloud.adapter.describe",
"hardware.operation.request"
]);
const rpcHandlers = new Map([
["system.health", handleSystemHealth],
["cloud.adapter.describe", handleAdapterDescribe],
["hardware.operation.request", handleHardwareOperationRequest]
]);
export async function handleJsonRpcRequest(envelope, context = {}) {
const requestMeta = extractMeta(envelope);
const requestId = envelope?.id ?? "req_unassigned";
const method = typeof envelope?.method === "string" ? envelope.method : "method_unspecified";
try {
validateRequest(envelope);
} catch (error) {
return createErrorEnvelope({
id: requestId,
code: ERROR_CODES.invalidRequest,
message: "Invalid JSON-RPC request",
data: {
reason: error.message
},
requestMeta,
context: {
...context,
operation: "json_rpc.validate",
target: {
type: "rpc_method",
id: method
}
}
});
}
const handler = rpcHandlers.get(envelope.method);
if (!handler) {
return createErrorEnvelope({
id: envelope.id,
code: ERROR_CODES.methodNotFound,
message: `Method ${envelope.method} is not implemented by hwlab-cloud-api`,
data: {
method: envelope.method
},
requestMeta: envelope.meta,
context: {
...context,
operation: envelope.method,
target: {
type: "rpc_method",
id: envelope.method
},
result: "rejected"
}
});
}
try {
const result = await handler(envelope.params ?? {}, envelope, context);
return createResultEnvelope({
id: envelope.id,
result,
requestMeta: envelope.meta
});
} catch (error) {
return createErrorEnvelope({
id: envelope.id,
code: ERROR_CODES.internalError,
message: "hwlab-cloud-api failed to process the request",
data: {
reason: error.message
},
requestMeta: envelope.meta,
context: {
...context,
operation: envelope.method,
target: {
type: "rpc_method",
id: envelope.method
},
result: "failed"
}
});
}
}
export function createResultEnvelope({ id, result, requestMeta = {} }) {
const envelope = {
jsonrpc: JSON_RPC_VERSION,
id,
result,
meta: createResponseMeta(requestMeta)
};
validateResponse(envelope);
return envelope;
}
export function createErrorEnvelope({
id = "req_unassigned",
code = ERROR_CODES.hwlabUnknown,
message = "HWLAB runtime error",
data = {},
requestMeta = {},
context = {}
} = {}) {
const responseId = id ?? "req_unassigned";
const meta = createResponseMeta(requestMeta);
const audit = createAuditRecord({
requestId: responseId,
actor: deriveActorFromMeta(requestMeta),
source: {
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
traceId: meta.traceId,
adapter: context.adapter || "json-rpc"
},
operation: context.operation || "json_rpc.error",
target: context.target || {
type: "cloud_api",
id: CLOUD_API_SERVICE_ID
},
result: context.result || "failed",
timestamp: context.timestamp
});
const envelope = {
jsonrpc: JSON_RPC_VERSION,
id: responseId,
error: {
code,
message,
data: {
...normalizeErrorData(data),
audit
}
},
meta
};
validateResponse(envelope);
return envelope;
}
export function createResponseMeta(requestMeta = {}) {
const meta = {
traceId: requestMeta.traceId || `trc_${randomUUID()}`,
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV
};
if (requestMeta.actorId) {
meta.actorId = requestMeta.actorId;
}
return meta;
}
function handleSystemHealth() {
return {
status: "ok",
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
db: {
connected: false,
mode: "not_configured"
}
};
}
function handleAdapterDescribe() {
return {
serviceId: CLOUD_API_SERVICE_ID,
rpcEndpoint: "POST /rpc",
restEndpoint: "POST /v1/rpc/{method}",
methods: SUPPORTED_RPC_METHODS,
auditFields: AUDIT_FIELD_NAMES,
db: {
connected: false,
mode: "migration_skeleton_only"
}
};
}
function handleHardwareOperationRequest(params, envelope, context) {
const projectId = params.projectId || "project_unspecified";
return {
accepted: false,
status: "skeleton",
reason: "cloud core L1 does not dispatch hardware operations yet",
audit: createAuditRecord({
requestId: envelope.id,
actor: deriveActorFromMeta(envelope.meta),
source: {
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
traceId: envelope.meta.traceId,
adapter: context.adapter || "json-rpc"
},
operation: "hardware.operation.request",
target: {
type: "project",
id: projectId
},
result: "rejected"
})
};
}
function extractMeta(envelope) {
if (envelope && typeof envelope === "object" && !Array.isArray(envelope) && envelope.meta) {
return envelope.meta;
}
return {};
}
function normalizeErrorData(data) {
if (!data || typeof data !== "object" || Array.isArray(data)) {
return {};
}
return data;
}
+64
View File
@@ -0,0 +1,64 @@
import assert from "node:assert/strict";
import test from "node:test";
import { AUDIT_FIELD_NAMES } from "../audit/index.mjs";
import { ERROR_CODES, validateResponse } from "../protocol/index.mjs";
import { handleJsonRpcRequest } from "./json-rpc.mjs";
test("unknown JSON-RPC method returns an error envelope with audit fields", async () => {
const response = await handleJsonRpcRequest({
jsonrpc: "2.0",
id: "req_01J00000000000000000000000",
method: "hardware.operation.cancel",
params: {
operationId: "op_01J00000000000000000000000"
},
meta: {
traceId: "trc_01J00000000000000000000000",
actorId: "usr_01J00000000000000000000000",
serviceId: "hwlab-cloud-web",
environment: "dev"
}
});
validateResponse(response);
assert.equal(response.error.code, ERROR_CODES.methodNotFound);
assert.equal(Object.hasOwn(response, "result"), false);
const audit = response.error.data.audit;
for (const field of AUDIT_FIELD_NAMES) {
assert.ok(Object.hasOwn(audit, field), `expected audit.${field}`);
}
assert.equal(audit.requestId, "req_01J00000000000000000000000");
assert.equal(audit.actor.id, "usr_01J00000000000000000000000");
assert.equal(audit.operation, "hardware.operation.cancel");
assert.equal(audit.target.type, "rpc_method");
assert.equal(audit.result, "rejected");
});
test("hardware.operation.request is a non-dispatching skeleton with visible audit fields", async () => {
const response = await handleJsonRpcRequest({
jsonrpc: "2.0",
id: "req_01J00000000000000000000001",
method: "hardware.operation.request",
params: {
projectId: "prj_01J00000000000000000000000"
},
meta: {
traceId: "trc_01J00000000000000000000001",
actorId: "usr_01J00000000000000000000000",
serviceId: "hwlab-cloud-web",
environment: "dev"
}
});
validateResponse(response);
assert.equal(response.result.accepted, false);
assert.equal(response.result.status, "skeleton");
for (const field of AUDIT_FIELD_NAMES) {
assert.ok(Object.hasOwn(response.result.audit, field), `expected audit.${field}`);
}
assert.equal(response.result.audit.operation, "hardware.operation.request");
assert.equal(response.result.audit.target.id, "prj_01J00000000000000000000000");
});
+276
View File
@@ -0,0 +1,276 @@
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { ENVIRONMENT_DEV, ERROR_CODES } from "../protocol/index.mjs";
import {
AUDIT_FIELD_NAMES,
CLOUD_API_SERVICE_ID,
createAuditRecord
} from "../audit/index.mjs";
import {
SUPPORTED_RPC_METHODS,
createErrorEnvelope,
handleJsonRpcRequest
} from "./json-rpc.mjs";
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
export function createCloudApiServer(options = {}) {
return createServer(async (request, response) => {
try {
await routeRequest(request, response, options);
} catch (error) {
sendJson(response, 500, {
error: {
code: "internal_error",
message: "hwlab-cloud-api request handling failed",
reason: error.message
}
});
}
});
}
export function buildHealthPayload() {
return {
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
status: "ok",
db: {
connected: false,
mode: "not_configured"
}
};
}
async function routeRequest(request, response, options) {
const url = new URL(request.url || "/", "http://hwlab-cloud-api.local");
if (request.method === "GET" && url.pathname === "/health") {
sendJson(response, 200, buildHealthPayload());
return;
}
if (request.method === "GET" && url.pathname === "/live") {
sendJson(response, 200, {
serviceId: CLOUD_API_SERVICE_ID,
status: "live"
});
return;
}
if (request.method === "POST" && (url.pathname === "/rpc" || url.pathname === "/json-rpc")) {
await handleRpcHttpRequest(request, response, options);
return;
}
if (url.pathname === "/v1" || url.pathname.startsWith("/v1/")) {
await handleRestAdapter(request, response, url, options);
return;
}
sendRestError(request, response, 404, "not_found", "Route is not part of the L1 cloud-api skeleton", {
operation: `${request.method || "GET"} ${url.pathname}`,
target: {
type: "route",
id: url.pathname
}
});
}
async function handleRpcHttpRequest(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let envelope;
try {
envelope = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(
response,
400,
createErrorEnvelope({
id: "req_unassigned",
code: ERROR_CODES.parseError,
message: "Invalid JSON body",
data: {
reason: error.message
},
context: {
operation: "json_rpc.parse",
target: {
type: "http_route",
id: request.url || "/rpc"
},
result: "rejected"
}
})
);
return;
}
const rpcResponse = await handleJsonRpcRequest(envelope, {
adapter: "json-rpc"
});
sendJson(response, statusForRpcResponse(rpcResponse), rpcResponse);
}
async function handleRestAdapter(request, response, url, options) {
if (request.method === "GET" && url.pathname === "/v1") {
sendJson(response, 200, {
serviceId: CLOUD_API_SERVICE_ID,
adapter: "rest",
status: "skeleton",
rpcBridge: "POST /v1/rpc/{method}",
methods: SUPPORTED_RPC_METHODS,
auditFields: AUDIT_FIELD_NAMES,
db: {
connected: false,
mode: "not_configured"
}
});
return;
}
if (request.method !== "POST" || !url.pathname.startsWith("/v1/rpc/")) {
sendRestError(request, response, 404, "not_found", "REST route is not implemented in the L1 skeleton", {
operation: `${request.method || "GET"} ${url.pathname}`,
target: {
type: "route",
id: url.pathname
}
});
return;
}
const method = decodeURIComponent(url.pathname.slice("/v1/rpc/".length));
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendRestError(request, response, 400, "parse_error", "Invalid JSON body", {
operation: method,
target: {
type: "rpc_method",
id: method
},
result: "rejected",
reason: error.message
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendRestError(request, response, 400, "invalid_params", "REST RPC bridge body must be a JSON object", {
operation: method,
target: {
type: "rpc_method",
id: method
},
result: "rejected"
});
return;
}
const rpcResponse = await handleJsonRpcRequest(
{
jsonrpc: "2.0",
id: getHeader(request, "x-request-id") || `req_${randomUUID()}`,
method,
params,
meta: {
traceId: getHeader(request, "x-trace-id") || `trc_${randomUUID()}`,
actorId: getHeader(request, "x-actor-id"),
serviceId: getHeader(request, "x-source-service-id") || CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV
}
},
{
adapter: "rest"
}
);
sendJson(response, statusForRpcResponse(rpcResponse), rpcResponse);
}
function sendRestError(request, response, statusCode, code, message, context = {}) {
const requestId = getHeader(request, "x-request-id") || "req_unassigned";
sendJson(response, statusCode, {
error: {
code,
message,
reason: context.reason,
audit: createAuditRecord({
requestId,
actor: {
type: "user",
id: getHeader(request, "x-actor-id") || `system_${CLOUD_API_SERVICE_ID}`
},
source: {
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
traceId: getHeader(request, "x-trace-id"),
adapter: "rest"
},
operation: context.operation || `${request.method || "GET"} ${request.url || "/"}`,
target: context.target || {
type: "route",
id: request.url || "/"
},
result: context.result || "failed"
})
}
});
}
function statusForRpcResponse(rpcResponse) {
if (!rpcResponse.error) {
return 200;
}
if (rpcResponse.error.code === ERROR_CODES.invalidRequest || rpcResponse.error.code === ERROR_CODES.parseError) {
return 400;
}
if (rpcResponse.error.code === ERROR_CODES.methodNotFound) {
return 404;
}
return 200;
}
function readBody(request, limitBytes = DEFAULT_BODY_LIMIT_BYTES) {
const limit = Number.isInteger(limitBytes) && limitBytes > 0 ? limitBytes : DEFAULT_BODY_LIMIT_BYTES;
return new Promise((resolve, reject) => {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
if (Buffer.byteLength(body, "utf8") > limit) {
request.destroy(new Error(`request body exceeds ${limit} bytes`));
}
});
request.on("end", () => resolve(body));
request.on("error", reject);
});
}
function sendJson(response, statusCode, body) {
response.writeHead(statusCode, {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store"
});
response.end(`${JSON.stringify(body)}\n`);
}
function getHeader(request, name) {
const value = request.headers[name.toLowerCase()];
if (Array.isArray(value)) {
return value[0];
}
return value;
}
+25
View File
@@ -0,0 +1,25 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createCloudApiServer } from "./server.mjs";
test("cloud api exposes /health and /live probes", async () => {
const server = createCloudApiServer();
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const health = await fetch(`http://127.0.0.1:${port}/health`);
assert.equal(health.status, 200);
assert.equal((await health.json()).serviceId, "hwlab-cloud-api");
const live = await fetch(`http://127.0.0.1:${port}/live`);
assert.equal(live.status, 200);
assert.equal((await live.json()).status, "live");
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
@@ -0,0 +1,115 @@
-- HWLAB L1 Cloud Core schema skeleton.
-- This file is a contract placeholder only; it does not connect to or write a
-- live database.
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS gateway_sessions (
id TEXT PRIMARY KEY,
project_id TEXT,
gateway_service_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
started_at TEXT,
ended_at TEXT
);
CREATE TABLE IF NOT EXISTS box_resources (
id TEXT PRIMARY KEY,
project_id TEXT,
gateway_session_id TEXT,
resource_state TEXT NOT NULL DEFAULT 'available',
labels_json TEXT NOT NULL DEFAULT '{}',
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS box_capabilities (
id TEXT PRIMARY KEY,
box_resource_id TEXT NOT NULL,
capability_type TEXT NOT NULL,
capability_json TEXT NOT NULL DEFAULT '{}',
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS wiring_configs (
id TEXT PRIMARY KEY,
project_id TEXT,
wiring_json TEXT NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'pending',
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS patch_panel_status (
id TEXT PRIMARY KEY,
gateway_session_id TEXT,
wiring_config_id TEXT,
status_json TEXT NOT NULL DEFAULT '{}',
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS hardware_operations (
id TEXT PRIMARY KEY,
project_id TEXT,
requested_by TEXT,
operation_type TEXT NOT NULL,
operation_json TEXT NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'requested',
requested_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS audit_events (
id TEXT PRIMARY KEY,
request_id TEXT NOT NULL,
actor TEXT NOT NULL,
source TEXT NOT NULL,
operation TEXT NOT NULL,
target TEXT NOT NULL,
result TEXT NOT NULL,
timestamp TEXT NOT NULL,
event_json TEXT NOT NULL DEFAULT '{}'
);
CREATE TABLE IF NOT EXISTS agent_sessions (
id TEXT PRIMARY KEY,
project_id TEXT,
agent_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
started_at TEXT,
ended_at TEXT
);
CREATE TABLE IF NOT EXISTS worker_sessions (
id TEXT PRIMARY KEY,
agent_session_id TEXT,
worker_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
started_at TEXT,
ended_at TEXT
);
CREATE TABLE IF NOT EXISTS agent_trace_events (
id TEXT PRIMARY KEY,
trace_id TEXT NOT NULL,
agent_session_id TEXT,
worker_session_id TEXT,
level TEXT NOT NULL DEFAULT 'info',
message TEXT NOT NULL DEFAULT '',
event_json TEXT NOT NULL DEFAULT '{}',
occurred_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS evidence_records (
id TEXT PRIMARY KEY,
project_id TEXT,
operation_id TEXT,
evidence_type TEXT NOT NULL,
uri TEXT NOT NULL DEFAULT '',
metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL
);
+21
View File
@@ -0,0 +1,21 @@
import { TABLES } from "../protocol/index.mjs";
export const CLOUD_CORE_MIGRATION_ID = "0001_cloud_core_skeleton";
export const FROZEN_CLOUD_CORE_TABLES = Object.freeze([...TABLES]);
export const CLOUD_CORE_MIGRATIONS = Object.freeze([
{
id: CLOUD_CORE_MIGRATION_ID,
path: "internal/db/migrations/0001_cloud_core_skeleton.sql",
tables: FROZEN_CLOUD_CORE_TABLES,
connectsToDatabase: false
}
]);
export function assertFrozenCloudCoreTables(tables = FROZEN_CLOUD_CORE_TABLES) {
const missing = TABLES.filter((table) => !tables.includes(table));
if (missing.length > 0) {
throw new Error(`cloud core schema is missing frozen tables: ${missing.join(", ")}`);
}
}
+36
View File
@@ -0,0 +1,36 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { TABLES } from "../protocol/index.mjs";
import {
CLOUD_CORE_MIGRATIONS,
FROZEN_CLOUD_CORE_TABLES,
assertFrozenCloudCoreTables
} from "./schema.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
test("cloud core schema keeps the frozen L0 table names", () => {
assert.deepEqual(FROZEN_CLOUD_CORE_TABLES, TABLES);
assertFrozenCloudCoreTables();
assert.equal(CLOUD_CORE_MIGRATIONS[0].connectsToDatabase, false);
});
test("initial migration skeleton declares every frozen table", async () => {
const sql = await readFile(path.join(repoRoot, "internal/db/migrations/0001_cloud_core_skeleton.sql"), "utf8");
for (const table of TABLES) {
assert.match(sql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}\\b`), `missing ${table}`);
}
assert.match(sql, /\brequest_id\b/);
assert.match(sql, /\bactor\b/);
assert.match(sql, /\bsource\b/);
assert.match(sql, /\boperation\b/);
assert.match(sql, /\btarget\b/);
assert.match(sql, /\bresult\b/);
assert.match(sql, /\btimestamp\b/);
});