feat: add phone frequency controls and modularize codebase
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import ts from 'typescript';
|
||||
|
||||
const root = process.cwd();
|
||||
const contract = JSON.parse(readFileSync(resolve(root, 'docs/contracts/operations-r2-methods.json'), 'utf8'));
|
||||
|
||||
function sha(value) {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
|
||||
function loadSource(relativePath) {
|
||||
const path = resolve(root, relativePath);
|
||||
const source = readFileSync(path, 'utf8');
|
||||
return { source, file: ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS) };
|
||||
}
|
||||
|
||||
function classMethods(relativePath, className) {
|
||||
const { source, file } = loadSource(relativePath);
|
||||
const declaration = file.statements.find(
|
||||
(statement) => ts.isClassDeclaration(statement) && statement.name?.text === className,
|
||||
);
|
||||
if (!declaration || !ts.isClassDeclaration(declaration)) throw new Error(`${className} not found`);
|
||||
return new Map(
|
||||
declaration.members.filter(ts.isMethodDeclaration).map((method) => {
|
||||
if (!method.body) throw new Error(`${className}.${method.name.getText(file)} has no body`);
|
||||
return [method.name.getText(file), {
|
||||
signatureSha256: sha(source.slice(method.getStart(file), method.body.getStart(file))),
|
||||
bodySha256: sha(method.body.getText(file)),
|
||||
}];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const queryClasses = {
|
||||
messages: ['api/src/operations/queries/messages.queries.ts', 'OperationsMessageQueries'],
|
||||
uplink: ['api/src/operations/queries/uplink.queries.ts', 'OperationsUplinkQueries'],
|
||||
dashboard: ['api/src/operations/queries/dashboard.queries.ts', 'OperationsDashboardQueries'],
|
||||
quality: ['api/src/operations/queries/quality.queries.ts', 'OperationsQualityQueries'],
|
||||
logs: ['api/src/operations/queries/logs.queries.ts', 'OperationsLogQueries'],
|
||||
downstream: ['api/src/operations/queries/downstream.queries.ts', 'OperationsDownstreamQueries'],
|
||||
trace: ['api/src/operations/queries/trace.queries.ts', 'OperationsTraceQueries'],
|
||||
};
|
||||
const queryMethods = new Map();
|
||||
for (const [group, [path, className]] of Object.entries(queryClasses)) {
|
||||
for (const [name, hashes] of classMethods(path, className)) {
|
||||
if (queryMethods.has(name)) throw new Error(`Duplicate R2 query method: ${name}`);
|
||||
queryMethods.set(name, { group, ...hashes });
|
||||
}
|
||||
}
|
||||
|
||||
const facadeMethods = classMethods('api/src/operations/operations.service.ts', 'OperationsService');
|
||||
for (const expected of contract.methods) {
|
||||
const queryMethod = queryMethods.get(expected.name);
|
||||
if (!queryMethod) throw new Error(`R2 query method missing: ${expected.name}`);
|
||||
if (queryMethod.group !== expected.group) throw new Error(`${expected.name}: moved to unexpected group`);
|
||||
if (queryMethod.signatureSha256 !== expected.signatureSha256) throw new Error(`${expected.name}: query signature changed`);
|
||||
if (queryMethod.bodySha256 !== expected.bodySha256) throw new Error(`${expected.name}: query behavior changed`);
|
||||
if (!expected.isPrivate) {
|
||||
const facadeMethod = facadeMethods.get(expected.name);
|
||||
if (!facadeMethod) throw new Error(`OperationsService facade method missing: ${expected.name}`);
|
||||
if (facadeMethod.signatureSha256 !== expected.signatureSha256) throw new Error(`${expected.name}: facade signature changed`);
|
||||
}
|
||||
}
|
||||
if (queryMethods.size !== contract.methods.length) throw new Error('Unexpected R2 query methods found');
|
||||
if (facadeMethods.size !== contract.methods.filter((method) => !method.isPrivate).length) {
|
||||
throw new Error('Unexpected OperationsService facade methods found');
|
||||
}
|
||||
|
||||
const contractsSource = loadSource('api/src/operations/operations.contracts.ts').file;
|
||||
const actualContracts = new Map(
|
||||
contractsSource.statements.filter(ts.isInterfaceDeclaration)
|
||||
.map((statement) => [statement.name.text, sha(statement.getText(contractsSource))]),
|
||||
);
|
||||
for (const [name, expectedHash] of Object.entries(contract.contracts)) {
|
||||
if (actualContracts.get(name) !== expectedHash) throw new Error(`Operations contract changed: ${name}`);
|
||||
}
|
||||
|
||||
const helpersSource = loadSource('api/src/operations/operations.helpers.ts').file;
|
||||
const actualHelpers = new Map();
|
||||
for (const statement of helpersSource.statements) {
|
||||
if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) {
|
||||
actualHelpers.set(statement.name.text, sha(statement.body.getText(helpersSource)));
|
||||
} else if (ts.isVariableStatement(statement)) {
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
actualHelpers.set(declaration.name.getText(helpersSource), sha(declaration.initializer?.getText(helpersSource) ?? ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [name, expectedHash] of Object.entries(contract.helpers)) {
|
||||
if (actualHelpers.get(name) !== expectedHash) throw new Error(`Operations helper changed: ${name}`);
|
||||
}
|
||||
if (actualHelpers.size !== Object.keys(contract.helpers).length) throw new Error('Unexpected Operations helpers found');
|
||||
|
||||
console.log(`R2 operations facade verified: ${facadeMethods.size} public methods, ${queryMethods.size - facadeMethods.size} private methods, ${actualHelpers.size} helpers; signatures and implementations unchanged.`);
|
||||
Reference in New Issue
Block a user