refactor: standardise antd imports and remove the avoidable any (#65)
Stage 1 of #65: this issue's original two lists. The type-checked gate it also owns follows in later stages. Nine files imported antd from the barrel while the rest of the codebase used deep imports from antd/es. Both resolve to the same modules under antd v5 and Vite, so this is not the tree-shaking problem it would have been under v4 — the cost was that a documented convention had two spellings, and nobody reading a file could tell whether its style was deliberate or just old. Eighty-two imports converted, and every antd/es path was checked to exist before generating any of them rather than trusting a name-mangling rule. The three `client: any` parameters in cartCheckout are now PoolClient. These functions run inside a transaction, and `any` removed exactly the check that would catch a pool-versus-client mix-up — which in this codebase means a query silently running outside the transaction it was meant to be part of, on the path that takes money. publicCustomer took `any` and now takes a CustomerRow describing what it actually reads. Typed as its own shape rather than the whole table so that adding a column later — a password hash, a token, an internal note — cannot quietly start being echoed back to a customer. The three `(window as any).paypal` casts are replaced by a declared interface for the injected SDK. It is deliberately narrow: it describes the three things this app calls, not the whole SDK, because a wider guess would be fiction and a wrong shape typed confidently is worse than an honest cast. The property is optional, since the SDK is absent until its script has loaded — which is the check both call sites already make. Verified: 141 unit, 169 integration and 94 end-to-end passing. The end-to-end run is the one that matters here — an antd import migration can build cleanly and still break at runtime through styles or context, so a green tsc proves less than it appears to. Lint drops from 8 warnings to 4 in the backend and 30 to 27 in the frontend, all of them the no-explicit-any this change removed. As a side effect the no-unsafe count that later stages exist to clear falls from 259 to 216 in the backend and 73 to 67 in the frontend, measured rather than estimated. Refs #65 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import type { PoolClient } from 'pg';
|
||||
import { pool } from '../db';
|
||||
import { asyncRoute } from '../asyncRoute';
|
||||
import { requireCustomer } from '../middleware/customerAuth';
|
||||
@@ -38,7 +39,7 @@ interface LockedCart {
|
||||
|
||||
// Locks the customer's cart, verifies every item is still reserved to them,
|
||||
// and returns { cartId, items: [{id, name, price_cents}], totalCents }.
|
||||
async function loadLockedCart(client: any, customerId: number): Promise<LockedCart | null> {
|
||||
async function loadLockedCart(client: PoolClient, customerId: number): Promise<LockedCart | null> {
|
||||
const { rows: cartRows } = await client.query(`SELECT id FROM carts WHERE customer_id = $1`, [customerId]);
|
||||
if (!cartRows.length) return null;
|
||||
const cartId = cartRows[0].id;
|
||||
@@ -64,7 +65,7 @@ type OpenedCheckout =
|
||||
// items. The caller owns the transaction — on `ok: false` it should roll back
|
||||
// and return the error as a 400.
|
||||
async function openCheckout(
|
||||
client: any,
|
||||
client: PoolClient,
|
||||
customerId: number,
|
||||
shippingAddressId: number,
|
||||
processor: string,
|
||||
@@ -145,7 +146,7 @@ router.post('/paypal/create', requireCustomer, asyncRoute(async (req: Request, r
|
||||
// Returns the sold item ids and the buyer, so the caller can notify favoriters
|
||||
// *after* COMMIT. Sending inside the transaction would email people about a
|
||||
// sale that then rolled back, and would hold the transaction open for SMTP.
|
||||
async function completeCheckout(client: any, checkoutId: number, processor: string, processorOrderId: string | null, rawEvent: unknown): Promise<{ itemIds: number[]; buyerId: number | null }> {
|
||||
async function completeCheckout(client: PoolClient, checkoutId: number, processor: string, processorOrderId: string | null, rawEvent: unknown): Promise<{ itemIds: number[]; buyerId: number | null }> {
|
||||
const { rows: checkoutItems } = await client.query(
|
||||
`SELECT item_id, price_cents FROM checkout_items WHERE checkout_id = $1`,
|
||||
[checkoutId]
|
||||
|
||||
@@ -32,7 +32,21 @@ async function createSession(customerId: number): Promise<string> {
|
||||
return token;
|
||||
}
|
||||
|
||||
function publicCustomer(c: any) {
|
||||
// The subset of a customers row that is safe to return to the customer it
|
||||
// belongs to. Typed as its own shape rather than `any` so that adding a column
|
||||
// to the table — a password hash, a token, an internal note — cannot silently
|
||||
// start being echoed back by a `...c` somewhere downstream.
|
||||
interface CustomerRow {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string | null;
|
||||
email_verified: boolean;
|
||||
marketing_consent: boolean;
|
||||
favorite_alerts: boolean;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
function publicCustomer(c: CustomerRow) {
|
||||
return {
|
||||
id: c.id,
|
||||
email: c.email,
|
||||
|
||||
+11
-1
@@ -1,5 +1,15 @@
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { Layout, Typography, Switch, Row, Col, Spin, Button, theme, Badge, Empty, Alert } from 'antd';
|
||||
import Layout from 'antd/es/layout';
|
||||
import Typography from 'antd/es/typography';
|
||||
import Switch from 'antd/es/switch';
|
||||
import Row from 'antd/es/row';
|
||||
import Col from 'antd/es/col';
|
||||
import Spin from 'antd/es/spin';
|
||||
import Button from 'antd/es/button';
|
||||
import theme from 'antd/es/theme';
|
||||
import Badge from 'antd/es/badge';
|
||||
import Empty from 'antd/es/empty';
|
||||
import Alert from 'antd/es/alert';
|
||||
import { ShoppingCartOutlined, FilterOutlined } from '@ant-design/icons';
|
||||
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { Item, FilterOptions, fetchItems, fetchFilterOptions } from './api';
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Layout, Table, Button, Drawer, Form, Input, InputNumber, Upload, Modal,
|
||||
Space, Tag, Typography, Switch, message, Image as AntImage, theme, Tabs,
|
||||
Select
|
||||
} from 'antd';
|
||||
import Layout from 'antd/es/layout';
|
||||
import Table from 'antd/es/table';
|
||||
import Button from 'antd/es/button';
|
||||
import Drawer from 'antd/es/drawer';
|
||||
import Form from 'antd/es/form';
|
||||
import Input from 'antd/es/input';
|
||||
import InputNumber from 'antd/es/input-number';
|
||||
import Upload from 'antd/es/upload';
|
||||
import Modal from 'antd/es/modal';
|
||||
import Space from 'antd/es/space';
|
||||
import Tag from 'antd/es/tag';
|
||||
import Typography from 'antd/es/typography';
|
||||
import Switch from 'antd/es/switch';
|
||||
import message from 'antd/es/message';
|
||||
import AntImage from 'antd/es/image';
|
||||
import theme from 'antd/es/theme';
|
||||
import Tabs from 'antd/es/tabs';
|
||||
import Select from 'antd/es/select';
|
||||
import { UploadOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Table, Drawer, Descriptions, Tag, Typography, Spin, Empty, Modal, Button, message } from 'antd';
|
||||
import Table from 'antd/es/table';
|
||||
import Drawer from 'antd/es/drawer';
|
||||
import Descriptions from 'antd/es/descriptions';
|
||||
import Tag from 'antd/es/tag';
|
||||
import Typography from 'antd/es/typography';
|
||||
import Spin from 'antd/es/spin';
|
||||
import Empty from 'antd/es/empty';
|
||||
import Modal from 'antd/es/modal';
|
||||
import Button from 'antd/es/button';
|
||||
import message from 'antd/es/message';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import {
|
||||
fetchCustomers, fetchCustomerDetail, fetchReservedItems, releaseReservedItem,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Form, InputNumber, Button, Typography, message, Card } from 'antd';
|
||||
import Form from 'antd/es/form';
|
||||
import InputNumber from 'antd/es/input-number';
|
||||
import Button from 'antd/es/button';
|
||||
import Typography from 'antd/es/typography';
|
||||
import message from 'antd/es/message';
|
||||
import Card from 'antd/es/card';
|
||||
import { fetchAdminSettings, updateAdminSettings } from './adminSettingsApi';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Layout, Typography, List, Button, Empty, Card, Radio, Form, Input,
|
||||
Checkbox, Modal, message, Tag, Spin, theme, Space
|
||||
} from 'antd';
|
||||
import Layout from 'antd/es/layout';
|
||||
import Typography from 'antd/es/typography';
|
||||
import List from 'antd/es/list';
|
||||
import Button from 'antd/es/button';
|
||||
import Empty from 'antd/es/empty';
|
||||
import Card from 'antd/es/card';
|
||||
import Radio from 'antd/es/radio';
|
||||
import Form from 'antd/es/form';
|
||||
import Input from 'antd/es/input';
|
||||
import Checkbox from 'antd/es/checkbox';
|
||||
import Modal from 'antd/es/modal';
|
||||
import message from 'antd/es/message';
|
||||
import Tag from 'antd/es/tag';
|
||||
import Spin from 'antd/es/spin';
|
||||
import theme from 'antd/es/theme';
|
||||
import Space from 'antd/es/space';
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import {
|
||||
@@ -109,9 +121,9 @@ export default function Cart() {
|
||||
useEffect(() => {
|
||||
if (!paypalReady || !selectedAddressId || items.length === 0) return;
|
||||
const container = document.getElementById('paypal-cart-buttons');
|
||||
if (!container || !(window as any).paypal) return;
|
||||
if (!container || !window.paypal) return;
|
||||
container.innerHTML = '';
|
||||
(window as any).paypal.Buttons({
|
||||
window.paypal.Buttons({
|
||||
createOrder: async () => {
|
||||
const { orderID } = await createCartPaypalOrder(selectedAddressId);
|
||||
return orderID;
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { Card, Badge, Typography, Carousel, Button, message, Tag, Modal, Tooltip } from 'antd';
|
||||
import Card from 'antd/es/card';
|
||||
import Badge from 'antd/es/badge';
|
||||
import Typography from 'antd/es/typography';
|
||||
import Carousel from 'antd/es/carousel';
|
||||
import Button from 'antd/es/button';
|
||||
import message from 'antd/es/message';
|
||||
import Tag from 'antd/es/tag';
|
||||
import Modal from 'antd/es/modal';
|
||||
import Tooltip from 'antd/es/tooltip';
|
||||
import { LeftOutlined, RightOutlined, HeartOutlined, HeartFilled } from '@ant-design/icons';
|
||||
import type { CarouselRef } from 'antd/es/carousel';
|
||||
import { Item } from '../api';
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Typography, Switch, Button, Table, Modal, message, Space, Divider } from 'antd';
|
||||
import Typography from 'antd/es/typography';
|
||||
import Switch from 'antd/es/switch';
|
||||
import Button from 'antd/es/button';
|
||||
import Table from 'antd/es/table';
|
||||
import Modal from 'antd/es/modal';
|
||||
import message from 'antd/es/message';
|
||||
import Space from 'antd/es/space';
|
||||
import Divider from 'antd/es/divider';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { fetchMyOrders, OrderHistoryItem, updateConsent, exportMyData, deleteMyAccount } from './customerApi';
|
||||
import { setFavoriteAlerts } from './favoritesApi';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Typography, Card } from 'antd';
|
||||
import Typography from 'antd/es/typography';
|
||||
import Card from 'antd/es/card';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import React, { useEffect, useState } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter, Routes, Route, useLocation, useNavigate } from 'react-router-dom';
|
||||
import type { Location } from 'react-router-dom';
|
||||
import { ConfigProvider, theme as antdTheme } from 'antd';
|
||||
import ConfigProvider from 'antd/es/config-provider';
|
||||
import antdTheme from 'antd/es/theme';
|
||||
import 'antd/dist/reset.css';
|
||||
import Button from 'antd/es/button';
|
||||
import ModalDialog from 'antd/es/modal';
|
||||
|
||||
+27
-1
@@ -1,7 +1,33 @@
|
||||
// The PayPal JS SDK is injected at runtime by the script tag below, so there is
|
||||
// no package to import types from. This declares the sliver of it this app
|
||||
// actually uses, rather than repeating `(window as any).paypal` at each call
|
||||
// site — three casts that each silently opted out of type checking.
|
||||
//
|
||||
// Deliberately narrow: it describes what is called here, not the whole SDK. A
|
||||
// wider guess would be fiction, and a wrong shape typed confidently is worse
|
||||
// than an honest `unknown`.
|
||||
export interface PaypalButtonsConfig {
|
||||
createOrder: () => Promise<string>;
|
||||
onApprove: (data: { orderID: string }) => Promise<void> | void;
|
||||
onError: (err: unknown) => void;
|
||||
}
|
||||
|
||||
export interface PaypalSdk {
|
||||
Buttons: (config: PaypalButtonsConfig) => { render: (selector: string) => void };
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
// Absent until the SDK script has loaded, which is what loadPaypalSdk and
|
||||
// every caller has to check before using it.
|
||||
paypal?: PaypalSdk;
|
||||
}
|
||||
}
|
||||
|
||||
let loadPromise: Promise<void> | null = null;
|
||||
|
||||
export function loadPaypalSdk(clientId: string, currency: string): Promise<void> {
|
||||
if ((window as any).paypal) return Promise.resolve();
|
||||
if (window.paypal) return Promise.resolve();
|
||||
if (loadPromise) return loadPromise;
|
||||
loadPromise = new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
|
||||
Reference in New Issue
Block a user