mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-06-18 02:08:46 -04:00
295 lines
7.1 KiB
TypeScript
295 lines
7.1 KiB
TypeScript
/**
|
|
* Shared PCD integration harness.
|
|
*/
|
|
|
|
import { assert } from '@std/assert';
|
|
import { TestClient } from '$test-harness/client.ts';
|
|
import { openDb } from '$test-harness/db.ts';
|
|
import { getDbPath } from '$test-harness/server.ts';
|
|
|
|
export type ConflictStrategy = 'override' | 'align' | 'ask';
|
|
export type OpOrigin = 'base' | 'user';
|
|
export type OpState = 'published' | 'draft' | 'superseded' | 'dropped';
|
|
|
|
export interface PcdTestContext {
|
|
port: number;
|
|
origin: string;
|
|
basePath: string;
|
|
dbPath: string;
|
|
dbId: number;
|
|
pcdPath: string;
|
|
client: TestClient;
|
|
}
|
|
|
|
export interface SetupPcdOptions {
|
|
port: number;
|
|
name: string;
|
|
uuid?: string;
|
|
conflictStrategy?: ConflictStrategy;
|
|
enabled?: boolean;
|
|
canWriteToBase?: boolean;
|
|
}
|
|
|
|
export interface SeedOperation {
|
|
sql: string;
|
|
metadata?: string | null;
|
|
desiredState?: string | null;
|
|
contentHash?: string | null;
|
|
}
|
|
|
|
export interface InsertOpOptions extends SeedOperation {
|
|
origin: OpOrigin;
|
|
state?: OpState;
|
|
source?: 'local' | 'repo' | 'import';
|
|
sequence?: number;
|
|
}
|
|
|
|
export interface OpRow {
|
|
id: number;
|
|
database_id: number;
|
|
origin: string;
|
|
state: string;
|
|
source: string;
|
|
sql: string;
|
|
sequence: number | null;
|
|
metadata: string | null;
|
|
desired_state: string | null;
|
|
content_hash: string | null;
|
|
superseded_by_op_id: number | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export type JsonObject = Record<string, unknown>;
|
|
|
|
export async function setupPcd(options: SetupPcdOptions): Promise<PcdTestContext> {
|
|
const origin = `http://localhost:${options.port}`;
|
|
const basePath = `./dist/integration-${options.port}`;
|
|
const dbPath = getDbPath(options.port);
|
|
const uuid = options.uuid ?? options.name;
|
|
const pcdPath = await createPcdRepo(basePath, uuid);
|
|
const dbId = createDatabaseInstance(dbPath, {
|
|
name: options.name,
|
|
uuid,
|
|
localPath: pcdPath,
|
|
conflictStrategy: options.conflictStrategy ?? 'ask',
|
|
enabled: options.enabled,
|
|
canWriteToBase: options.canWriteToBase
|
|
});
|
|
|
|
return {
|
|
port: options.port,
|
|
origin,
|
|
basePath,
|
|
dbPath,
|
|
dbId,
|
|
pcdPath,
|
|
client: new TestClient(origin)
|
|
};
|
|
}
|
|
|
|
export async function createPcdRepo(basePath: string, uuid: string): Promise<string> {
|
|
const pcdPath = `${basePath}/data/databases/${uuid}`;
|
|
const schemaDir = `${pcdPath}/deps/schema/ops`;
|
|
await Deno.mkdir(schemaDir, { recursive: true });
|
|
|
|
const schema = await Deno.readTextFile('docs/backend/0.schema.sql');
|
|
await Deno.writeTextFile(`${schemaDir}/0.schema.sql`, schema);
|
|
|
|
return pcdPath;
|
|
}
|
|
|
|
function createDatabaseInstance(
|
|
dbPath: string,
|
|
opts: {
|
|
name: string;
|
|
uuid: string;
|
|
localPath: string;
|
|
conflictStrategy: ConflictStrategy;
|
|
enabled?: boolean;
|
|
canWriteToBase?: boolean;
|
|
}
|
|
): number {
|
|
const db = openDb(dbPath);
|
|
try {
|
|
db.exec(
|
|
`INSERT INTO database_instances (
|
|
uuid, name, repository_url, local_path,
|
|
sync_strategy, auto_pull, enabled, is_private,
|
|
personal_access_token, local_ops_enabled, conflict_strategy
|
|
) VALUES (?, ?, '', ?, 60, 0, ?, 0, ?, ?, ?)`,
|
|
[
|
|
opts.uuid,
|
|
opts.name,
|
|
opts.localPath,
|
|
opts.enabled !== false ? 1 : 0,
|
|
opts.canWriteToBase ? 'test-token' : null,
|
|
opts.canWriteToBase ? 0 : 1,
|
|
opts.conflictStrategy
|
|
]
|
|
);
|
|
const row = db.prepare('SELECT last_insert_rowid() AS id').get() as { id: number };
|
|
db.exec('UPDATE setup_state SET default_database_linked = 1 WHERE id = 1');
|
|
return row.id;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
export function insertOp(ctx: PcdTestContext, opts: InsertOpOptions): number {
|
|
const db = openDb(ctx.dbPath);
|
|
try {
|
|
db.exec(
|
|
`INSERT INTO pcd_ops (
|
|
database_id, origin, state, source,
|
|
sql, sequence, metadata, desired_state, content_hash
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
ctx.dbId,
|
|
opts.origin,
|
|
opts.state ?? 'published',
|
|
opts.source ?? 'local',
|
|
opts.sql,
|
|
opts.sequence ?? null,
|
|
opts.metadata ?? null,
|
|
opts.desiredState ?? null,
|
|
opts.contentHash ?? null
|
|
]
|
|
);
|
|
const row = db.prepare('SELECT last_insert_rowid() AS id').get() as { id: number };
|
|
return row.id;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
export function seedBase(ctx: PcdTestContext, operations: Array<string | SeedOperation>): number[] {
|
|
let sequence = nextSequence(ctx, 'base');
|
|
return operations.map((operation) => {
|
|
const seed = normalizeSeedOperation(operation);
|
|
return insertOp(ctx, {
|
|
...seed,
|
|
origin: 'base',
|
|
state: 'published',
|
|
sequence: sequence++
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function compilePcd(ctx: PcdTestContext): Promise<void> {
|
|
const draftId = insertOp(ctx, {
|
|
origin: 'base',
|
|
state: 'draft',
|
|
sql: 'SELECT 1;',
|
|
sequence: nextSequence(ctx, 'base')
|
|
});
|
|
|
|
const response = await ctx.client.postForm(
|
|
`/databases/${ctx.dbId}/changes?/drop`,
|
|
{ opIds: String(draftId) },
|
|
{ headers: { Origin: ctx.origin } }
|
|
);
|
|
|
|
assert(
|
|
response.status >= 200 && response.status < 400,
|
|
`Expected compile trigger to return 2xx/3xx, got ${response.status}`
|
|
);
|
|
}
|
|
|
|
export function opCheckpoint(ctx: PcdTestContext): number {
|
|
const db = openDb(ctx.dbPath);
|
|
try {
|
|
const row = db
|
|
.prepare('SELECT COALESCE(MAX(id), 0) AS id FROM pcd_ops WHERE database_id = ?')
|
|
.get(ctx.dbId) as { id: number };
|
|
return row.id;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
export function queryOpsSince(
|
|
ctx: PcdTestContext,
|
|
checkpoint: number,
|
|
filters: { origin?: OpOrigin; state?: OpState } = {}
|
|
): OpRow[] {
|
|
const db = openDb(ctx.dbPath);
|
|
try {
|
|
const clauses = ['database_id = ?', 'id > ?'];
|
|
const params: Array<string | number> = [ctx.dbId, checkpoint];
|
|
if (filters.origin) {
|
|
clauses.push('origin = ?');
|
|
params.push(filters.origin);
|
|
}
|
|
if (filters.state) {
|
|
clauses.push('state = ?');
|
|
params.push(filters.state);
|
|
}
|
|
return db
|
|
.prepare(`SELECT * FROM pcd_ops WHERE ${clauses.join(' AND ')} ORDER BY id`)
|
|
.all(...params) as OpRow[];
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
export function setFailOnReferencedDelete(ctx: PcdTestContext, enabled: boolean): void {
|
|
const db = openDb(ctx.dbPath);
|
|
try {
|
|
db.exec(
|
|
`UPDATE general_settings
|
|
SET fail_on_referenced_delete = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = 1`,
|
|
[enabled ? 1 : 0]
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
export function parseMetadata(op: OpRow): JsonObject {
|
|
return parseJsonObject(op.metadata);
|
|
}
|
|
|
|
export function parseDesiredState(op: OpRow): JsonObject {
|
|
return parseJsonObject(op.desired_state);
|
|
}
|
|
|
|
export function normalizeSql(sql: string): string {
|
|
return sql.replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
function nextSequence(ctx: PcdTestContext, origin: OpOrigin): number {
|
|
const db = openDb(ctx.dbPath);
|
|
try {
|
|
const row = db
|
|
.prepare(
|
|
`SELECT COALESCE(MAX(sequence), 0) + 1 AS sequence
|
|
FROM pcd_ops
|
|
WHERE database_id = ?
|
|
AND origin = ?`
|
|
)
|
|
.get(ctx.dbId, origin) as { sequence: number };
|
|
return row.sequence;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
function normalizeSeedOperation(operation: string | SeedOperation): SeedOperation {
|
|
if (typeof operation === 'string') {
|
|
return { sql: operation };
|
|
}
|
|
return operation;
|
|
}
|
|
|
|
function parseJsonObject(value: string | null): JsonObject {
|
|
if (!value) return {};
|
|
const parsed = JSON.parse(value) as unknown;
|
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
return parsed as JsonObject;
|
|
}
|
|
return {};
|
|
}
|