55 lines
6.8 KiB
JavaScript
55 lines
6.8 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const contracts = [
|
|
['dashboard', 'overview', '/dashboard/overview', 'readonly', 'dashboard'],
|
|
['gas', 'gas_basic', '/gas/gas_basic', 'writable', 'list'], ['gas', 'gas_account', '/gas/gas_account', 'writable', 'list'],
|
|
['delivery', 'delivery_basic', '/delivery/delivery_basic', 'writable', 'list'], ['delivery', 'delivery_account', '/delivery/delivery_account', 'writable', 'list'], ['delivery', 'delivery_task', '/delivery/delivery_task', 'writable', 'list'], ['delivery', 'delivery_track', '/delivery/delivery_track', 'writable', 'list'], ['delivery', 'delivery_track_point', '/delivery/delivery_track_point', 'writable', 'list'],
|
|
['staff', 'staff_account', '/staff/account', 'writable', 'list'], ['staff', 'staff_credential', '/staff/credential', 'writable', 'list'],
|
|
['user', 'user_account', '/user/account', 'writable', 'list'], ['user', 'user_address', '/user/address', 'writable', 'list'], ['user', 'user_service_relation', '/user/service_relation', 'writable', 'list'],
|
|
['device', 'dev_smart_cylinder_valve', '/device/dev_smart_cylinder_valve', 'writable', 'list'], ['device', 'dev_device_binding', '/device/dev_device_binding', 'writable', 'list'], ['device', 'dev_telemetry', '/device/dev_telemetry', 'readonly', 'list'],
|
|
['safety', 'saf_rule', '/safety/saf_rule', 'writable', 'list'], ['safety', 'saf_event', '/safety/saf_event', 'writable', 'list'], ['safety', 'saf_inspection', '/safety/saf_inspection', 'writable', 'list'],
|
|
['ec', 'ec_category', '/ec/ec_category', 'writable', 'list'], ['ec', 'ec_product', '/ec/ec_product', 'writable', 'list'], ['ec', 'ec_product_attribute', '/ec/ec_product_attribute', 'writable', 'list'], ['ec', 'ec_product_image', '/ec/ec_product_image', 'writable', 'list'], ['ec', 'ec_cart', '/ec/ec_cart', 'writable', 'list'], ['ec', 'ec_order', '/ec/ec_order', 'writable', 'list'], ['ec', 'ec_order_item', '/ec/ec_order_item', 'writable', 'list'], ['ec', 'ec_review', '/ec/ec_review', 'writable', 'list'],
|
|
['finance', 'fin_payment', '/finance/fin_payment', 'writable', 'list'], ['finance', 'fin_settlement', '/finance/fin_settlement', 'writable', 'list'], ['finance', 'fin_reconciliation', '/finance/fin_reconciliation', 'writable', 'list'],
|
|
['content', 'cnt_content', '/content/cnt_content', 'writable', 'list'], ['notification', 'ntf_template', '/notification/ntf_template', 'writable', 'list'], ['customer_service', 'cs_ticket', '/customer_service/cs_ticket', 'writable', 'list'],
|
|
['platform', 'platfrom_account', '/platform/platfrom_account', 'writable', 'list'], ['platform', 'platform_role', '/platform/platform_role', 'writable', 'list'], ['platform', 'platform_menu', '/platform/platform_menu', 'writable', 'tree'],
|
|
['wallet', 'wallet', '/wallet/wallet', 'readonly', 'list'], ['wallet', 'wallet_ledger', '/wallet/wallet_ledger', 'readonly', 'list'], ['wallet', 'wallet_recharge', '/wallet/wallet_recharge', 'readonly', 'list'], ['wallet', 'wallet_withdrawal', '/wallet/wallet_withdrawal', 'readonly', 'list'],
|
|
['report', 'report', '/report/report', 'readonly', 'list'], ['report', 'report_item', '/report/report_item', 'readonly', 'list'], ['report', 'report_metric_snapshot', '/report/report_metric_snapshot', 'readonly', 'list'],
|
|
['audit', 'aud_operation_log', '/audit/aud_operation_log', 'readonly', 'list'], ['audit', 'aud_export_log', '/audit/aud_export_log', 'readonly', 'list'], ['audit', 'aud_approval', '/audit/aud_approval', 'readonly', 'list'],
|
|
];
|
|
|
|
const resourceSource = fs.readFileSync('src/api/resources.ts', 'utf8');
|
|
const resourceEntry = (name, resource, mode, pageKind) => new RegExp(`name: '${name}', resource: '${resource}', title: '[^']+', mode: '${mode}', pageKind: '${pageKind}'`);
|
|
const files = (dir) => fs.existsSync(dir) ? fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? files(path.join(dir, entry.name)) : [path.join(dir, entry.name)]) : [];
|
|
|
|
function checkContracts() {
|
|
const failures = [];
|
|
if (contracts.length !== 46) failures.push(`catalogue: expected 46 resources, got ${contracts.length}`);
|
|
for (const [domain, name, resource, mode, pageKind] of contracts) {
|
|
const label = `${domain}/${name}`;
|
|
if (!resourceEntry(name, resource, mode, pageKind).test(resourceSource)) failures.push(`${label}: missing or incorrect api contract`);
|
|
const route = path.join('src/router/routes/modules', domain === 'dashboard' ? 'platform.ts' : `${domain}.ts`);
|
|
if (!fs.existsSync(route) || !fs.readFileSync(route, 'utf8').includes(name)) failures.push(`${label}: missing route`);
|
|
const page = pageKind === 'dashboard' ? path.join('src/views/dashboard/DashboardPage.vue') : path.join('src/views', domain, name, pageKind === 'tree' ? 'TreePage.vue' : 'ListPage.vue');
|
|
if (!fs.existsSync(page)) failures.push(`${label}: missing view`);
|
|
if (mode === 'readonly' && pageKind !== 'dashboard' && (!fs.existsSync(page) || !fs.readFileSync(page, 'utf8').includes('ReadOnlyListPage'))) failures.push(`${label}: readonly page exposes write surface`);
|
|
}
|
|
const readonlyPage = fs.readFileSync('src/views/shared/ReadOnlyListPage.vue', 'utf8');
|
|
if (/(CrudListPage|resourceApi\.(create|update|archive|updateStatus))/.test(readonlyPage)) failures.push('readonly shared page: contains write surface');
|
|
const treePage = fs.readFileSync('src/views/shared/TreePage.vue', 'utf8');
|
|
if (!treePage.includes('<a-tree') || !treePage.includes('parent_identity') || treePage.includes('CrudListPage')) failures.push('tree shared page: missing tree semantics');
|
|
const crudSource = fs.readFileSync('src/views/shared/CrudListPage.vue', 'utf8');
|
|
if (!crudSource.includes("key !== 'id'") || !crudSource.includes("key.endsWith('_id')")) failures.push('detail view: does not remove internal identifiers');
|
|
const identityLeaks = files('src/api').concat(files('src/views')).filter((file) => /\.(ts|vue)$/.test(file) && !file.endsWith('CrudListPage.vue') && !file.endsWith('ReadOnlyListPage.vue')).filter((file) => /\b[A-Za-z][A-Za-z0-9]*_id\b|\bid\b/.test(fs.readFileSync(file, 'utf8')));
|
|
if (identityLeaks.length) failures.push(`identity leak: ${identityLeaks.join(', ')}`);
|
|
if (resourceSource.includes('saf_event_disposal') || files('src/router/routes/modules').some((file) => fs.readFileSync(file, 'utf8').includes('saf_event_disposal'))) failures.push('saf_event_disposal: must not be displayed as an independent resource');
|
|
const obsoletePaths = /\/(?:commerce|staff\/staff_|user\/user_)/;
|
|
const stalePathFiles = files('src/api').concat(files('src/views')).filter((file) => /\.(ts|vue)$/.test(file) && obsoletePaths.test(fs.readFileSync(file, 'utf8')));
|
|
if (stalePathFiles.length) failures.push(`obsolete resource path: ${stalePathFiles.join(', ')}`);
|
|
return failures;
|
|
}
|
|
|
|
const failures = checkContracts();
|
|
for (const failure of failures) console.log(failure);
|
|
if (failures.length) process.exitCode = 1;
|