feat: implement PouchDB storage layer with CRUD operations and add showcase UI component
This commit is contained in:
@@ -1,2 +1,5 @@
|
||||
VITE_API_BASE_URL=http://localhost:8000/api
|
||||
VITE_APP_ENV=development
|
||||
VITE_COUCHDB_BASE_URL=http://202.146.229.134:7700
|
||||
VITE_COUCHDB_USERNAME=root
|
||||
VITE_COUCHDB_PASSWORD=password
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"@repo/utils": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"dayjs": "^1.11.19",
|
||||
"events": "^3.3.0",
|
||||
"i18next": "^24.2.2",
|
||||
"lucide-react": "^1.17.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-i18next": "^15.4.0",
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Title,
|
||||
Text,
|
||||
Table,
|
||||
Badge,
|
||||
} from '@repo/ui/components';
|
||||
import { itemDB, posConfigDB } from '../../core/db';
|
||||
import type { Item, POSConfiguration } from '../../core/db/types';
|
||||
|
||||
export default function PouchSample() {
|
||||
const [configs, setConfigs] = useState<POSConfiguration[]>([]);
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
|
||||
// Load initial data
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const allConfigs = await posConfigDB.find({ selector: {} });
|
||||
setConfigs(allConfigs);
|
||||
|
||||
const allItems = await itemDB.find({ selector: {} });
|
||||
setItems(allItems);
|
||||
console.log({allConfigs, allItems})
|
||||
} catch (err) {
|
||||
console.error('Failed to load PouchDB data', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 1. Initial Data Load
|
||||
loadData();
|
||||
|
||||
// 2. Setup Real-Time Listeners
|
||||
const unsubscribeItems = itemDB.onChange(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
const unsubscribePos = posConfigDB.onChange(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 3. CRITICAL: Cleanup to prevent memory leaks on unmount
|
||||
return () => {
|
||||
unsubscribeItems();
|
||||
unsubscribePos();
|
||||
};
|
||||
}, [loadData]);
|
||||
|
||||
// ─── POS Configuration Handlers ─────────────────────────────────
|
||||
|
||||
const handleSeedConfig = async () => {
|
||||
try {
|
||||
const id = `pos-${Date.now()}`;
|
||||
await posConfigDB.create({
|
||||
_id: id,
|
||||
pos_number: '1111111111666',
|
||||
pos_name: 'Premium Test POS TESTING COUNCH',
|
||||
items: items, // mapping current items
|
||||
payment_methods: [{ id: 'cash', name: 'Cash' }],
|
||||
});
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to seed config', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteConfig = async (id: string) => {
|
||||
try {
|
||||
await posConfigDB.delete(id);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete config', err);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Items Inventory Handlers ───────────────────────────────────
|
||||
|
||||
const handleAddItem = async () => {
|
||||
try {
|
||||
const id = `item-${Date.now()}`;
|
||||
await itemDB.create({
|
||||
_id: id,
|
||||
name: 'PLAYGROUND ALL DAY TESTING POUCH',
|
||||
base_price: '75000',
|
||||
item_type: 'wahana',
|
||||
usage_type: 'ticket',
|
||||
item_category: [{ name: 'Entertainment' }],
|
||||
item_rates: [
|
||||
{ season_period: 'weekday', price: 50000 },
|
||||
{ season_period: 'weekend', price: 75000 },
|
||||
],
|
||||
});
|
||||
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to add item', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteItem = async (id: string) => {
|
||||
try {
|
||||
await itemDB.delete(id);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete item', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearAll = async () => {
|
||||
try {
|
||||
await posConfigDB.cleanAllData();
|
||||
await itemDB.cleanAllData();
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to clear data', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<Group justify="space-between">
|
||||
<Title order={2}>Enterprise PouchDB Sync</Title>
|
||||
<Button color="error" variant="outline" onClick={handleClearAll}>
|
||||
Clear All Local Data
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Items Inventory Table */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Items Database</Title>
|
||||
<Button onClick={handleAddItem} color="success">
|
||||
Inject Mock ERP Item
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<div className="max-h-[400px] overflow-y-auto border border-gray-200 rounded-lg scrollbar-thin scrollbar-thumb-gray-300">
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">ID</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Name</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Type</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Base Price</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Rates Count</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{items.length > 0 ? (
|
||||
items.map((item) => (
|
||||
<Table.Tr key={item._id}>
|
||||
<Table.Td>{item._id}</Table.Td>
|
||||
<Table.Td>{item.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="brand" variant="light">
|
||||
{item.item_type}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>${Number(item.base_price).toFixed(2)}</Table.Td>
|
||||
<Table.Td>{item.item_rates?.length || 0}</Table.Td>
|
||||
<Table.Td>
|
||||
<Button size="xs" color="error" variant="subtle" onClick={() => handleDeleteItem(item._id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={6} align="center">
|
||||
<Text c="dimmed">No items found.</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* POS Configuration Table */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>POS Configurations</Title>
|
||||
<Button onClick={handleSeedConfig} variant="light" color="brand">
|
||||
Inject Mock POS Config
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<div className="max-h-[400px] overflow-y-auto border border-gray-200 rounded-lg scrollbar-thin scrollbar-thumb-gray-300">
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">ID</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">POS Name</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">POS Number</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Mapped Items</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{configs.length > 0 ? (
|
||||
configs.map((cfg) => (
|
||||
<Table.Tr key={cfg._id}>
|
||||
<Table.Td>{cfg._id}</Table.Td>
|
||||
<Table.Td>{cfg.pos_name}</Table.Td>
|
||||
<Table.Td>{cfg.pos_number}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="info" variant="outline">
|
||||
{cfg.items?.length || 0} Items
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Button size="xs" color="error" variant="subtle" onClick={() => handleDeleteConfig(cfg._id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={5} align="center">
|
||||
<Text c="dimmed">No configurations found.</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { ColorSchemeType, DensityType } from '@repo/ui/provider';
|
||||
import {
|
||||
Button,
|
||||
@@ -18,10 +19,15 @@ import {
|
||||
Table,
|
||||
Badge,
|
||||
Divider,
|
||||
Tabs,
|
||||
Box,
|
||||
Paper,
|
||||
} from '@repo/ui/components';
|
||||
import { ShieldCheck, Database, Lock, Layout, Activity, Printer } from 'lucide-react';
|
||||
import PrinterList from './printer-list';
|
||||
import ExamplePage from './example/example.page';
|
||||
import EventsDemoPage from './events-demo';
|
||||
import PouchSample from './pouch-sample';
|
||||
|
||||
interface ShowcaseViewProps {
|
||||
colorScheme: ColorSchemeType;
|
||||
@@ -31,6 +37,8 @@ interface ShowcaseViewProps {
|
||||
}
|
||||
|
||||
export default function ShowcaseView({ colorScheme, setColorScheme, density, setDensity }: ShowcaseViewProps) {
|
||||
const [activeTab, setActiveTab] = useState<string | null>('ui-components');
|
||||
|
||||
// Mock data for the table
|
||||
const tableData = [
|
||||
{ id: 'ORD-001', customer: 'John Doe', status: 'Shipped', total: '$120.00' },
|
||||
@@ -38,154 +46,170 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
|
||||
{ id: 'ORD-003', customer: 'Acme Corp', status: 'Delivered', total: '$1,250.00' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Container size="lg" py="xl">
|
||||
<Stack gap="xl">
|
||||
<Title order={1}>Super App UI Showcase</Title>
|
||||
const getSubtitle = () => {
|
||||
switch (activeTab) {
|
||||
case 'rbac': return 'Role-Based Access Control and Permissions';
|
||||
case 'storage': return 'Offline-First PouchDB Synchronization';
|
||||
case 'auth': return 'Authentication & Security Layers';
|
||||
case 'ui-components': return 'Theme, Typography, Forms & Data Grids';
|
||||
case 'events': return 'Global Event Bus Synchronization';
|
||||
case 'hardware': return 'Hardware Integration & Printers';
|
||||
default: return 'Architecture Showcase';
|
||||
}
|
||||
};
|
||||
|
||||
{/* =========================================
|
||||
CONTROL PANEL
|
||||
========================================= */}
|
||||
return (
|
||||
<Box className="min-h-screen" style={{ backgroundColor: 'var(--mantine-color-body)' }}>
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
placement="left"
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
variant="pills"
|
||||
radius="md"
|
||||
className="h-screen"
|
||||
styles={{
|
||||
root: { display: 'flex', height: '100vh', overflow: 'hidden' },
|
||||
list: {
|
||||
minWidth: 260,
|
||||
padding: '1rem',
|
||||
borderRight: '1px solid var(--mantine-color-default-border)',
|
||||
backgroundColor: 'var(--mantine-color-default-element-bg)'
|
||||
},
|
||||
panel: { flex: 1, display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' },
|
||||
tab: { justifyContent: 'flex-start' }, // Ensure the entire tab content aligns left
|
||||
tabLabel: { textAlign: 'left', flex: 1 }, // Ensure the text pushes to fill and aligns left
|
||||
}}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Box mb="xl" px="sm">
|
||||
<Title order={3} c="brand.7">Eigen ERP</Title>
|
||||
<Text size="xs" c="dimmed">Architecture Showcase</Text>
|
||||
</Box>
|
||||
|
||||
<Tabs.Tab value="ui-components" leftSection={<Layout size={18} />}>
|
||||
UI Components
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="storage" leftSection={<Database size={18} />}>
|
||||
Offline Storage
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="rbac" leftSection={<ShieldCheck size={18} />}>
|
||||
RBAC Engine
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="auth" leftSection={<Lock size={18} />}>
|
||||
Auth & Security
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="events" leftSection={<Activity size={18} />}>
|
||||
Events
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="hardware" leftSection={<Printer size={18} />}>
|
||||
Hardware
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value={activeTab as string}>
|
||||
{/* Header */}
|
||||
<Paper
|
||||
p="md"
|
||||
radius={0}
|
||||
withBorder
|
||||
style={{
|
||||
borderTop: 0,
|
||||
borderLeft: 0,
|
||||
borderRight: 0,
|
||||
zIndex: 10,
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={0}>
|
||||
<Title order={3}>Architecture Showcase</Title>
|
||||
<Text size="sm" c="dimmed">{getSubtitle()}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
{/* Scrollable Content Area */}
|
||||
<Box className="flex-1 overflow-y-auto p-6" style={{ height: 'calc(100vh - 80px)' }}>
|
||||
<Container size="xl" m={0} p={0}>
|
||||
|
||||
{/* --- UI COMPONENTS TAB --- */}
|
||||
{activeTab === 'ui-components' && (
|
||||
<Stack gap="xl">
|
||||
{/* Control Panel */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Theme Controls
|
||||
</Title>
|
||||
<Title order={4} mb="md">Theme Controls</Title>
|
||||
<Group grow align="flex-end">
|
||||
<Select
|
||||
label="Color Scheme"
|
||||
value={colorScheme}
|
||||
onChange={(val: string | null) => setColorScheme((val as ColorSchemeType) || 'light')}
|
||||
data={[
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
]}
|
||||
onChange={(val) => setColorScheme((val as ColorSchemeType) || 'light')}
|
||||
data={[{ value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' }]}
|
||||
/>
|
||||
<Select
|
||||
label="Density (Spacing & Sizing)"
|
||||
value={density}
|
||||
onChange={(val: string | null) => setDensity((val as DensityType) || 'standard')}
|
||||
data={[
|
||||
{ value: 'compact', label: 'Compact (ERP Mode)' },
|
||||
{ value: 'standard', label: 'Standard (UI Mode)' },
|
||||
]}
|
||||
onChange={(val) => setDensity((val as DensityType) || 'standard')}
|
||||
data={[{ value: 'compact', label: 'Compact (ERP Mode)' }, { value: 'standard', label: 'Standard (UI Mode)' }]}
|
||||
/>
|
||||
</Group>
|
||||
</Card>
|
||||
|
||||
{/* =========================================
|
||||
TAILWIND V4 BRIDGE TEST
|
||||
========================================= */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Tailwind v4 Synchronization
|
||||
</Title>
|
||||
{/* This div purely uses Tailwind classes to prove it inherits Mantine's variables */}
|
||||
<div className="bg-brand-500 text-brand-50 p-md rounded-md shadow-md text-base">
|
||||
<span className="font-bold">Tailwind works!</span> The padding (p-md), border-radius (rounded-md), text size
|
||||
(text-base), and background color of this box are entirely controlled by the ThemeProvider's current state.
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* =========================================
|
||||
TYPOGRAPHY & BUTTONS
|
||||
========================================= */}
|
||||
{/* Typography & Buttons */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Title order={4} mb="xs">
|
||||
Typography & Badges
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
This is dimmed small text indicating a subtitle.
|
||||
</Text>
|
||||
<Text mb="md">
|
||||
This is standard text describing the components below. Watch how the font changes when you switch
|
||||
density.
|
||||
</Text>
|
||||
<Group>
|
||||
<Title order={4} mb="xs">Typography & Badges</Title>
|
||||
<Text size="sm" c="dimmed">This is dimmed small text indicating a subtitle.</Text>
|
||||
<Group mt="md">
|
||||
<Badge color="brand">Brand Badge</Badge>
|
||||
<Badge color="success" variant="light">
|
||||
Success Status
|
||||
</Badge>
|
||||
<Badge color="error" variant="outline">
|
||||
Error State
|
||||
</Badge>
|
||||
<Badge color="success" variant="light">Success Status</Badge>
|
||||
<Badge color="error" variant="outline">Error State</Badge>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div>
|
||||
<Title order={4} mb="md">
|
||||
Buttons
|
||||
</Title>
|
||||
<Title order={4} mb="md">Buttons</Title>
|
||||
<Group>
|
||||
<Button variant="filled" color="brand">
|
||||
Filled Button
|
||||
</Button>
|
||||
<Button variant="outline" color="brand">
|
||||
Outline Button
|
||||
</Button>
|
||||
<Button variant="light" color="info">
|
||||
Light Info
|
||||
</Button>
|
||||
<Button variant="subtle" color="error">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="filled" color="brand">Filled Button</Button>
|
||||
<Button variant="outline" color="brand">Outline Button</Button>
|
||||
<Button variant="light" color="info">Light Info</Button>
|
||||
<Button variant="subtle" color="error">Cancel</Button>
|
||||
</Group>
|
||||
</div>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* =========================================
|
||||
COMPLEX FORMS (ERP STYLE)
|
||||
========================================= */}
|
||||
{/* Forms */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Form Elements
|
||||
</Title>
|
||||
<Title order={4} mb="md">Form Elements</Title>
|
||||
<Stack gap="md">
|
||||
<Group grow align="flex-start">
|
||||
<TextInput label="First Name" placeholder="Enter your first name" withAsterisk />
|
||||
<TextInput label="Last Name" placeholder="Enter your last name" />
|
||||
</Group>
|
||||
|
||||
<Group grow align="flex-start">
|
||||
<NumberInput label="Age" placeholder="25" min={0} max={100} />
|
||||
<PasswordInput label="Password" placeholder="Your secret password" withAsterisk />
|
||||
</Group>
|
||||
|
||||
<Textarea label="Bio" placeholder="Tell us about yourself" minRows={3} />
|
||||
|
||||
<Group mt="sm">
|
||||
<Checkbox label="I agree to the terms and conditions" defaultChecked />
|
||||
<Switch label="Enable notifications" defaultChecked />
|
||||
</Group>
|
||||
|
||||
<Radio.Group name="favoriteFramework" label="Select your favorite framework" withAsterisk>
|
||||
<Group mt="xs">
|
||||
<Radio value="react" label="React" />
|
||||
<Radio value="svelte" label="Svelte" />
|
||||
<Radio value="vue" label="Vue" />
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* =========================================
|
||||
DATA GRID / TABLE
|
||||
========================================= */}
|
||||
{/* Data Grid */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Data Grid
|
||||
</Title>
|
||||
<Title order={4} mb="md">Data Grid</Title>
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Order ID</Table.Th>
|
||||
<Table.Th>Customer</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Total Amount</Table.Th>
|
||||
<Table.Th>Total</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
@@ -194,10 +218,7 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
|
||||
<Table.Td>{row.id}</Table.Td>
|
||||
<Table.Td>{row.customer}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
size="sm"
|
||||
color={row.status === 'Delivered' ? 'success' : row.status === 'Shipped' ? 'info' : 'warning'}
|
||||
>
|
||||
<Badge size="sm" color={row.status === 'Delivered' ? 'success' : row.status === 'Shipped' ? 'info' : 'warning'}>
|
||||
{row.status}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
@@ -207,25 +228,63 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- STORAGE TAB --- */}
|
||||
{activeTab === 'storage' && (
|
||||
<Stack gap="xl">
|
||||
<PouchSample />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- RBAC TAB --- */}
|
||||
{activeTab === 'rbac' && (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">RBAC Engine</Title>
|
||||
<Text c="dimmed">RBAC Demo Component Coming Soon...</Text>
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- AUTH TAB --- */}
|
||||
{activeTab === 'auth' && (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">Auth & Security</Title>
|
||||
<Text c="dimmed">Auth Demo Component Coming Soon...</Text>
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- EVENTS TAB --- */}
|
||||
{activeTab === 'events' && (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">Nested Showcase Example</Title>
|
||||
<Text mb="md">This is an example of a nested showcase component.</Text>
|
||||
<ExamplePage />
|
||||
</Card>
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<EventsDemoPage />
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- HARDWARE TAB --- */}
|
||||
{activeTab === 'hardware' && (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<PrinterList />
|
||||
</Card>
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Nested Showcase Example
|
||||
</Title>
|
||||
<Text>
|
||||
This is an example of a nested showcase component. You can create multiple layers of showcases to organize
|
||||
features by domain or complexity.
|
||||
</Text>
|
||||
<ExamplePage />
|
||||
</Card>
|
||||
|
||||
{/* =========================================
|
||||
EVENT BUS SHOWCASE
|
||||
========================================= */}
|
||||
<EventsDemoPage />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
</Container>
|
||||
</Box>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Multi-Database PouchDB Configuration for apps/web.
|
||||
*
|
||||
* This module demonstrates the IoC pattern: the consuming app decides
|
||||
* which databases to create and where they sync to. The core engine
|
||||
* (`PouchDatabaseManager`) has zero knowledge of business domains.
|
||||
*/
|
||||
import { PouchDatabaseManager } from '@repo/core-storage';
|
||||
import { ENV } from '../../environment/env';
|
||||
import type { Item, POSConfiguration } from './types';
|
||||
|
||||
// ─── Manager Singleton ──────────────────────────────────────────
|
||||
|
||||
export const dbManager = new PouchDatabaseManager();
|
||||
|
||||
// ─── Helper: Build Secure Remote URL ────────────────────────────
|
||||
|
||||
function buildRemoteUrl(dbName: string): string | undefined {
|
||||
const { COUCHDB_BASE_URL, COUCHDB_USERNAME, COUCHDB_PASSWORD } = ENV;
|
||||
|
||||
if (!COUCHDB_BASE_URL || !COUCHDB_USERNAME || !COUCHDB_PASSWORD) {
|
||||
console.warn(`[DB Config] CouchDB credentials missing — "${dbName}" will run in offline-only mode.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 1. Tambahkan http:// secara otomatis jika DevOps hanya mengisi IP Address di .env
|
||||
const safeBaseUrl = COUCHDB_BASE_URL.startsWith('http')
|
||||
? COUCHDB_BASE_URL
|
||||
: `http://${COUCHDB_BASE_URL}`;
|
||||
|
||||
try {
|
||||
// 2. Gunakan URL parser yang aman dari karakter aneh pada password
|
||||
const url = new URL(safeBaseUrl);
|
||||
url.username = encodeURIComponent(COUCHDB_USERNAME);
|
||||
url.password = encodeURIComponent(COUCHDB_PASSWORD);
|
||||
url.pathname = `/${dbName}`;
|
||||
|
||||
return url.toString();
|
||||
} catch (error) {
|
||||
console.error(`[DB Config] Invalid URL format for CouchDB:`, safeBaseUrl);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Register Application Databases ─────────────────────────────
|
||||
|
||||
/** POS Configuration database — stores device settings, theme, etc. */
|
||||
export const posConfigDB = dbManager.register<POSConfiguration>({
|
||||
localName: 'pos_configuration',
|
||||
remoteUrl: buildRemoteUrl('pos_configuration'),
|
||||
});
|
||||
|
||||
/** Items database — products available for sale in POS. */
|
||||
export const itemDB = dbManager.register<Item>({
|
||||
localName: 'item',
|
||||
remoteUrl: buildRemoteUrl('item'),
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Enterprise ERP Data Domain Models
|
||||
* These types reflect the actual schema of the underlying CouchDB instances.
|
||||
*/
|
||||
|
||||
export interface ItemRate {
|
||||
season_period?: string | null;
|
||||
price: string | number;
|
||||
}
|
||||
|
||||
export interface ItemCategory {
|
||||
_id?: string;
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a sellable product or service.
|
||||
*/
|
||||
export interface Item {
|
||||
_id: string;
|
||||
_rev?: string;
|
||||
name: string;
|
||||
base_price: string | number;
|
||||
item_type: string;
|
||||
usage_type?: string;
|
||||
item_category?: ItemCategory[] | ItemCategory | string;
|
||||
item_rates?: ItemRate[];
|
||||
|
||||
// Allow for other ERP-specific fields
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the configuration and assigned data for a specific Point of Sale terminal.
|
||||
*/
|
||||
export interface POSConfiguration {
|
||||
_id: string;
|
||||
_rev?: string;
|
||||
pos_number: string;
|
||||
pos_name: string;
|
||||
items: Item[];
|
||||
payment_methods?: any[];
|
||||
|
||||
// Allow for other ERP-specific fields
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -6,5 +6,9 @@ export const ENV = {
|
||||
API_BASE_URL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api',
|
||||
APP_ENV: (import.meta.env.VITE_APP_ENV || 'development') as 'development' | 'staging' | 'production',
|
||||
IS_PROD: import.meta.env.VITE_APP_ENV === 'production',
|
||||
} as const;
|
||||
|
||||
// CouchDB Connection
|
||||
COUCHDB_BASE_URL: import.meta.env.VITE_COUCHDB_BASE_URL || 'http://localhost:5984',
|
||||
COUCHDB_USERNAME: import.meta.env.VITE_COUCHDB_USERNAME || '',
|
||||
COUCHDB_PASSWORD: import.meta.env.VITE_COUCHDB_PASSWORD || '',
|
||||
} as const;
|
||||
|
||||
@@ -8,4 +8,14 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
strictPort: true, // Fail if 5173 is in use. Electron NEEDS this exact port.
|
||||
},
|
||||
define: {
|
||||
// Crucial for PouchDB to not crash in the browser
|
||||
global: 'window',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// Force Vite to use the installed npm package for 'events'
|
||||
events: 'events',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
+98
-125
@@ -2,167 +2,140 @@
|
||||
|
||||
[← Back to Root](../../README.md)
|
||||
|
||||
The **Enterprise-grade storage engine** for the monorepo.
|
||||
This package provides an **Offline-First Storage Engine** using PouchDB, tailored for Enterprise React applications. It is built to seamlessly sync with remote CouchDB instances, providing full fault tolerance and offline capabilities.
|
||||
|
||||
This package provides a unified, Factory-based interface for interacting with browser storage (`localStorage` and `IndexedDB`). It enforces strict type safety, **Runtime Validation**, App Autonomy (Inversion of Control), and automatically provides **AES encryption at rest** for sensitive payloads (like access tokens) using `@repo/utils`.
|
||||
## High-Level Overview
|
||||
|
||||
---
|
||||
|
||||
## Architecture & Data Flow
|
||||
Our storage architecture enforces strict **Inversion of Control (IoC)**. The core engine (`@repo/core-storage`) is a pure factory—it knows absolutely nothing about your business domains, data models, or specific databases. Consuming applications (like `apps/web`) dictate the rules by injecting their specific configurations and generic types into the storage engine.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Apps ["apps/* (App Autonomy)"]
|
||||
REG[[AppStorageKey & App Registries]]
|
||||
UI[React Components / API Interceptors]
|
||||
INST{{Storage Instances}}
|
||||
subgraph UI ["Consuming App (apps/*)"]
|
||||
COMP["React Components / Forms"]
|
||||
end
|
||||
|
||||
subgraph Core ["@repo/core-storage (Engine Factories)"]
|
||||
API[IStorageService API]
|
||||
FAC[createLocalStorage / createIndexedDB]
|
||||
VAL{Runtime Gatekeeper}
|
||||
ENC{{AES Encryption Pipeline}}
|
||||
LOCAL[LocalStorage Adapter]
|
||||
IDB[IndexedDB Adapter]
|
||||
subgraph CoreStorage ["@repo/core-storage Engine"]
|
||||
MGR["PouchDatabaseManager Factory"]
|
||||
L_SALES[("Local PouchDB: Sales")]
|
||||
L_INV[("Local PouchDB: Inventory")]
|
||||
end
|
||||
|
||||
subgraph Browser ["Browser APIs (Native)"]
|
||||
B_LOCAL[(localStorage)]
|
||||
B_IDB[(IndexedDB)]
|
||||
subgraph RemoteServer ["CouchDB Cluster (Cloud/On-Prem)"]
|
||||
R_SALES[("Remote CouchDB: sales_db")]
|
||||
R_INV[("Remote CouchDB: inventory_db")]
|
||||
end
|
||||
|
||||
REG -.->|Injects Keys & Config| FAC
|
||||
FAC --> INST
|
||||
UI -->|getItem / setItem| INST
|
||||
INST --> API
|
||||
API --> VAL
|
||||
COMP -->|Read / Write| L_SALES
|
||||
COMP -->|Read / Write| L_INV
|
||||
MGR -->|Instantiates Multi-DB| L_SALES
|
||||
MGR -->|Instantiates Multi-DB| L_INV
|
||||
|
||||
VAL -.->|Valid Key?| ENC
|
||||
VAL -.->|Invalid Key!| ERR[Throws Security Exception]
|
||||
|
||||
ENC -.->|Sensitive Key| LOCAL & IDB
|
||||
VAL -.->|Plain-text Key| LOCAL & IDB
|
||||
|
||||
LOCAL <--> B_LOCAL
|
||||
IDB <--> B_IDB
|
||||
|
||||
%% Styling Subgraphs
|
||||
style Apps fill:#e7f5ff,stroke:#74c0fc,stroke-width:2px,color:#1864ab
|
||||
style Core fill:#f8f9fa,stroke:#ced4da,stroke-width:2px,color:#495057
|
||||
style Browser fill:#f1f3f5,stroke:#ced4da,stroke-width:2px,color:#495057
|
||||
L_SALES <-->|Native Sync Live and Retry| R_SALES
|
||||
L_INV <-->|Native Sync Live and Retry| R_INV
|
||||
|
||||
%% Styling Nodes
|
||||
style UI fill:#339af0,stroke:#1864ab,color:#fff
|
||||
style REG fill:#1864ab,stroke:#1864ab,color:#fff
|
||||
style INST fill:#339af0,stroke:#1864ab,color:#fff
|
||||
style API fill:#845ef7,stroke:#5f3dc4,color:#fff
|
||||
style FAC fill:#845ef7,stroke:#5f3dc4,color:#fff
|
||||
|
||||
%% Gatekeeper is GREEN (Security Checkpoint), Error is RED
|
||||
style VAL fill:#20c997,stroke:#089981,color:#fff
|
||||
style ERR fill:#fa5252,stroke:#c92a2a,color:#fff
|
||||
|
||||
style LOCAL fill:#845ef7,stroke:#5f3dc4,color:#fff
|
||||
style IDB fill:#845ef7,stroke:#5f3dc4,color:#fff
|
||||
style ENC fill:#fab005,stroke:#e67700,color:#fff
|
||||
style B_LOCAL fill:#868e96,stroke:#495057,color:#fff
|
||||
style B_IDB fill:#868e96,stroke:#495057,color:#fff
|
||||
style MGR fill:#339af0,stroke:#1864ab,color:#fff
|
||||
style L_SALES fill:#845ef7,stroke:#5f3dc4,color:#fff
|
||||
style L_INV fill:#845ef7,stroke:#5f3dc4,color:#fff
|
||||
style R_SALES fill:#fab005,stroke:#e67700,color:#fff
|
||||
style R_INV fill:#fab005,stroke:#e67700,color:#fff
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Primary Goals & Architectural Principles
|
||||
## Core Concepts & Usage
|
||||
|
||||
* **App Autonomy (Inversion of Control)**: The core storage engine does not know about your application's keys. Consuming applications define their own keys, their own `encryptedKeys` sets, and their own `plainTextKeys` sets, injecting them into the factory upon instantiation.
|
||||
* **Runtime Gatekeeper (Defensive Programming)**: The engine validates every `setItem`, `getItem`, and `removeItem` operation. If an app attempts to access a key that wasn't explicitly registered in `encryptedKeys` or `plainTextKeys`, the engine will immediately throw a Security Exception to prevent rogue data access/injection.
|
||||
* **Dual Backend Strategy**:
|
||||
* `createLocalStorage`: Ideal for small, synchronous-like data (tokens, user preferences, settings).
|
||||
* `createIndexedDB`: Built for large, asynchronous data (offline drafts, cached API responses, large arrays/blobs) bypassing the 5MB localStorage limit.
|
||||
* **Security by Default**: Developers don't need to manually encrypt/decrypt data. If a key is passed in the `encryptedKeys` configuration, the engine handles AES encryption transparently.
|
||||
* **Corrupt Data Resilience**: If parsing or decryption fails (e.g., tampered data or changed encryption keys), the corrupt entry is safely removed and returns `null`, preventing the app from crashing.
|
||||
### 1. Initialization & Registration (`PouchDatabaseManager`)
|
||||
|
||||
---
|
||||
The `PouchDatabaseManager` acts as the IoC Factory. Apps use it to register and initialize multiple discrete PouchDB databases using a `PouchConfig`.
|
||||
|
||||
## 🚀 App-Level Setup & Usage
|
||||
|
||||
### 1. Define App Keys and Instantiate (Inversion of Control)
|
||||
|
||||
In your consuming application (e.g., `apps/web/src/core/storage/index.ts`), define your keys and use the factories to create your instances.
|
||||
**Why we use this pattern:** Instead of scattering raw database instantiations across the codebase, the manager centralizes connections. If a database is requested twice, the manager efficiently returns the exact same instance.
|
||||
|
||||
```typescript
|
||||
// apps/web/src/core/storage/index.ts
|
||||
import { createLocalStorage, createIndexedDB } from '@repo/core-storage';
|
||||
import { PouchDatabaseManager } from '@repo/core-storage';
|
||||
import type { Item } from './types';
|
||||
|
||||
// 1. Define Keys
|
||||
export const AppStorageKey = {
|
||||
USER_PROFILE: 'user_profile',
|
||||
ACCESS_TOKEN: 'access_token',
|
||||
LOCALE: 'app_locale',
|
||||
} as const;
|
||||
export const dbManager = new PouchDatabaseManager();
|
||||
|
||||
export type AppStorageKeyValue = (typeof AppStorageKey)[keyof typeof AppStorageKey];
|
||||
|
||||
// 2. Classify Keys
|
||||
export const ENCRYPTED_KEYS = new Set<AppStorageKeyValue>([
|
||||
AppStorageKey.USER_PROFILE,
|
||||
AppStorageKey.ACCESS_TOKEN,
|
||||
]);
|
||||
|
||||
export const PLAIN_KEYS = new Set<AppStorageKeyValue>([
|
||||
AppStorageKey.LOCALE,
|
||||
]);
|
||||
|
||||
// 3. Instantiate Factories
|
||||
export const secureStorage = createLocalStorage<AppStorageKeyValue>({
|
||||
encryptedKeys: ENCRYPTED_KEYS,
|
||||
plainTextKeys: PLAIN_KEYS
|
||||
});
|
||||
|
||||
export const secureIndexedDB = createIndexedDB<AppStorageKeyValue>({
|
||||
dbName: 'eigen_erp_db',
|
||||
storeName: 'web_store',
|
||||
encryptedKeys: ENCRYPTED_KEYS,
|
||||
plainTextKeys: PLAIN_KEYS
|
||||
// Register a strictly-typed database with bi-directional sync
|
||||
export const itemDB = dbManager.register<Item>({
|
||||
localName: 'items_db',
|
||||
remoteUrl: 'http://admin:password@localhost:5984/items_db'
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Usage in App Components
|
||||
### 2. CRUD & Queries (`PouchDatabaseWrapper`)
|
||||
|
||||
Now, you can import your locally-created instances anywhere in your app.
|
||||
When you register a database, you receive a strictly typed `PouchDatabaseWrapper`. This wrapper abstracts away the raw PouchDB API, giving developers clean, Promise-based helper methods without ever needing to pass `dbName` or complex identifiers repeatedly.
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `create(data)` | Inserts a new document. PouchDB will auto-generate an `_id` if omitted. |
|
||||
| `update(id, data)` | Automatically fetches the latest `_rev` to merge the payload, preventing conflict errors. |
|
||||
| `delete(id)` | Automatically fetches the latest `_rev` to safely remove the document. |
|
||||
| `getOne(id)` | Retrieves a single document by its `_id`. |
|
||||
| `getAll()` | Retrieves all documents, automatically filtering out internal `_design/` docs. |
|
||||
| `find(options)` | Queries using MongoDB-style selectors (via `pouchdb-find`). |
|
||||
|
||||
**Example of `find()` with Selectors:**
|
||||
Instead of pulling all documents into memory and filtering them with JavaScript, we leverage native MongoDB-style selectors for performance:
|
||||
|
||||
```typescript
|
||||
import { secureStorage, AppStorageKey } from '@/core/storage';
|
||||
import type { UserProfile } from '@/types';
|
||||
|
||||
// CREATE / UPDATE
|
||||
// Since USER_PROFILE is in ENCRYPTED_KEYS, it is AES-encrypted automatically.
|
||||
await secureStorage.setItem(AppStorageKey.USER_PROFILE, {
|
||||
id: 1,
|
||||
name: 'Firman',
|
||||
role: 'admin'
|
||||
const expensiveItems = await itemDB.find({
|
||||
selector: {
|
||||
price: { $gt: 100 },
|
||||
category: 'electronics'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
// READ (Returns null if not found or if decryption fails)
|
||||
const profile = await secureStorage.getItem<UserProfile>(AppStorageKey.USER_PROFILE);
|
||||
if (profile) {
|
||||
console.log('Welcome back,', profile.name);
|
||||
### 3. Real-Time Reactivity (The `onChange` Pub/Sub Pattern)
|
||||
|
||||
**CRITICAL CONCEPT:** We do **not** expose the raw `db.changes()` feed directly to React components. Instead, the `PouchDatabaseWrapper` utilizes a clean Pub/Sub abstraction via the `.onChange(callback)` method.
|
||||
|
||||
**Why we use this pattern:**
|
||||
1. **Memory Safety:** Direct bindings to PouchDB's raw changes feed often lead to zombie listeners and memory leaks. The `.onChange()` returns an unsubscribe function natively tailored for React's `useEffect` cleanup block.
|
||||
2. **Connection Efficiency:** It maintains a *single* WebSocket/Polling connection to the database under the hood. Multiple React components can subscribe to the same wrapper without opening dozens of parallel database connections.
|
||||
|
||||
```tsx
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
import { itemDB } from '../core/db';
|
||||
|
||||
export function InventoryList() {
|
||||
const [items, setItems] = useState([]);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
const data = await itemDB.getAll();
|
||||
setItems(data);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 1. Initial Load
|
||||
loadData();
|
||||
|
||||
// 2. Subscribe to local mutations AND remote CouchDB syncs
|
||||
const unsubscribe = itemDB.onChange(() => {
|
||||
console.log('Database updated locally or remotely. Refreshing...');
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 3. Prevent memory leaks!
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [loadData]);
|
||||
|
||||
// UI rendering...
|
||||
}
|
||||
|
||||
// DELETE
|
||||
await secureStorage.removeItem(AppStorageKey.USER_PROFILE);
|
||||
```
|
||||
|
||||
### 3. The Runtime Gatekeeper
|
||||
### 4. CouchDB Sync & CORS Troubleshooting
|
||||
|
||||
If you try to access an unregistered key, the engine protects the app by throwing an error at runtime:
|
||||
|
||||
```typescript
|
||||
// Throws Error: "[Storage Engine] Security Exception: Key 'rogue_key' is not registered..."
|
||||
await secureStorage.setItem('rogue_key' as any, 'hacked');
|
||||
```
|
||||
|
||||
---
|
||||
By providing a `remoteUrl` to the manager, the engine automatically handles bi-directional synchronization in the background (`live: true, retry: true`). If the server goes down, the local app will continue working seamlessly and sync automatically when the connection is restored.
|
||||
|
||||
> [!WARNING]
|
||||
> **Migration Hazard**: If you move an existing key from `plainTextKeys` to `encryptedKeys` (or vice versa), existing users will experience a parsing failure on their next session (because the library expects encrypted data but finds plain text). The resilient parser will catch this and gracefully clear the key, which may effectively log them out or reset their local preference.
|
||||
> **CORS Infinite Retries & Preflight Failures**
|
||||
> If your browser blocks the synchronization with a CORS error, you will see PouchDB enter an infinite retry loop in the network tab.
|
||||
>
|
||||
> **Do NOT try to fix this in the frontend code!**
|
||||
> This is exclusively a CouchDB server configuration issue. You must enable CORS directly on the CouchDB instance by editing its `local.ini` or using its dashboard configuration to allow origins, credentials, and headers.
|
||||
@@ -13,12 +13,23 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/utils": "workspace:*"
|
||||
"@repo/utils": "workspace:*",
|
||||
"pouchdb-browser": "^9.0.0",
|
||||
"pouchdb-find": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/pouchdb": "^6.4.2",
|
||||
"@types/pouchdb-adapter-memory": "^6.1.6",
|
||||
"@types/pouchdb-browser": "^6.1.5",
|
||||
"@types/pouchdb-core": "^7.0.15",
|
||||
"@types/pouchdb-find": "^7.3.3",
|
||||
"@types/pouchdb-mapreduce": "^6.1.10",
|
||||
"eslint": "^8.57.1",
|
||||
"pouchdb-adapter-memory": "^9.0.0",
|
||||
"pouchdb-core": "^9.0.0",
|
||||
"pouchdb-mapreduce": "^9.0.0",
|
||||
"typescript": "5.5.4",
|
||||
"vitest": "^4.0.17"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
export type { IStorageService } from './storage.interface';
|
||||
export type { StorageOptions } from './local-storage.service';
|
||||
export type { IndexedDBConfig } from './indexed-db.service';
|
||||
export type { PouchConfig } from './pouch';
|
||||
|
||||
// ─── Service Classes ────────────────────────────────────────────
|
||||
export { LocalStorageService, createLocalStorage } from './local-storage.service';
|
||||
export { IndexedDBService, createIndexedDB } from './indexed-db.service';
|
||||
export { PouchDatabaseManager, PouchDatabaseWrapper } from './pouch';
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import PouchDB from 'pouchdb-browser';
|
||||
import PouchDBFind from 'pouchdb-find';
|
||||
|
||||
// Register the find plugin globally
|
||||
PouchDB.plugin(PouchDBFind);
|
||||
|
||||
// ─── Configuration Interface ────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Configuration for creating a new PouchDB database instance.
|
||||
* Follows Inversion of Control — the consuming app decides names and remote URLs.
|
||||
*/
|
||||
export interface PouchConfig {
|
||||
/** Name of the local PouchDB database (stored in IndexedDB by the browser). */
|
||||
localName: string;
|
||||
/**
|
||||
* Optional remote CouchDB URL for bi-directional live sync.
|
||||
* Should include credentials if authentication is required.
|
||||
* Example: `http://user:password@host:port/db_name`
|
||||
*/
|
||||
remoteUrl?: string;
|
||||
}
|
||||
|
||||
// ─── PouchDatabaseWrapper ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Object-Oriented wrapper around a single PouchDB instance.
|
||||
* Provides strictly-typed CRUD + query helpers so developers never interact
|
||||
* with the raw PouchDB API or pass `dbName` repeatedly.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const itemDB = dbManager.register({ localName: 'items' });
|
||||
* await itemDB.create({ _id: 'item-001', name: 'Widget', price: 9.99 });
|
||||
* const results = await itemDB.find({ selector: { price: { $gt: 5 } } });
|
||||
* ```
|
||||
*/
|
||||
export class PouchDatabaseWrapper<DefaultType extends object = any> {
|
||||
/** The underlying raw PouchDB instance (escape hatch for advanced usage). */
|
||||
readonly raw: PouchDB.Database;
|
||||
|
||||
private syncHandler: PouchDB.Replication.Sync<object> | null = null;
|
||||
private listeners = new Set<() => void>();
|
||||
private changesFeed: PouchDB.Core.Changes<object> | null = null;
|
||||
|
||||
constructor(config: PouchConfig) {
|
||||
this.raw = new PouchDB(config.localName);
|
||||
|
||||
// Set up bi-directional live sync if a remote URL is provided
|
||||
if (config.remoteUrl) {
|
||||
this.syncHandler = this.raw.sync(config.remoteUrl, {
|
||||
live: true,
|
||||
retry: true,
|
||||
});
|
||||
|
||||
// Fault-tolerant error handling — prevents app crashes when CouchDB is unreachable
|
||||
this.syncHandler.on('error', (err: unknown) => {
|
||||
console.warn(`[PouchDB Sync] Error on "${config.localName}":`, err);
|
||||
});
|
||||
|
||||
this.syncHandler.on('paused', (info: unknown) => {
|
||||
if (info) {
|
||||
console.warn(`[PouchDB Sync] Paused on "${config.localName}":`, info);
|
||||
}
|
||||
});
|
||||
|
||||
this.syncHandler.on('denied', (err: unknown) => {
|
||||
console.warn(`[PouchDB Sync] Denied on "${config.localName}":`, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── CRUD Operations ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Create a new document. If `_id` is not provided in data, PouchDB generates one.
|
||||
*/
|
||||
async create<T extends object = DefaultType>(data: T): Promise<PouchDB.Core.Response> {
|
||||
return this.raw.put(data as PouchDB.Core.Document<T>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing document by ID.
|
||||
* Automatically fetches the latest `_rev` to prevent conflict errors.
|
||||
*/
|
||||
async update<T extends object = DefaultType>(id: string, data: Partial<T>): Promise<PouchDB.Core.Response> {
|
||||
const existing = await this.raw.get(id);
|
||||
const merged = { ...existing, ...data };
|
||||
return this.raw.put(merged);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a document by ID.
|
||||
* Automatically fetches the latest `_rev` before removal.
|
||||
*/
|
||||
async delete(id: string): Promise<PouchDB.Core.Response> {
|
||||
const doc = await this.raw.get(id);
|
||||
return this.raw.remove(doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single document by ID.
|
||||
*/
|
||||
async getOne<T = DefaultType>(id: string): Promise<T & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta> {
|
||||
return this.raw.get<T>(id) as Promise<T & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all documents from the database.
|
||||
* Returns a clean array of document objects (excludes PouchDB design docs).
|
||||
*/
|
||||
async getAll<T = DefaultType>(): Promise<T[]> {
|
||||
const result = await this.raw.allDocs({ include_docs: true });
|
||||
return result.rows
|
||||
.filter((row) => !row.id.startsWith('_design/'))
|
||||
.map((row) => row.doc as unknown as T);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve multiple documents by their IDs.
|
||||
* Returns a clean array of found documents (silently skips missing/errored entries).
|
||||
*/
|
||||
async getSome<T = DefaultType>(ids: string[]): Promise<T[]> {
|
||||
const result = await this.raw.allDocs({ keys: ids, include_docs: true });
|
||||
return result.rows
|
||||
.filter((row): row is PouchDB.Core.AllDocsResponse<object>['rows'][number] => !('error' in row) && !!(row as any).doc)
|
||||
.map((row) => (row as any).doc as T);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query documents using MongoDB-style selectors (powered by `pouchdb-find`).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const electronics = await itemDB.find({ selector: { category: 'electronics' } });
|
||||
* const expensive = await itemDB.find({ selector: { price: { $gt: 100 } }, limit: 10 });
|
||||
* ```
|
||||
*/
|
||||
async find<T extends object = DefaultType>(options: PouchDB.Find.FindRequest<T>): Promise<T[]> {
|
||||
const result = await this.raw.find(options as PouchDB.Find.FindRequest<object>);
|
||||
return result.docs as unknown as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all documents from the database while keeping the database itself intact.
|
||||
* Useful for "clear cache" or "reset local data" flows.
|
||||
*/
|
||||
async cleanAllData(): Promise<void> {
|
||||
const result = await this.raw.allDocs();
|
||||
const deletions = result.rows
|
||||
.filter((row) => !row.id.startsWith('_design/'))
|
||||
.map((row) => ({
|
||||
_id: row.id,
|
||||
_rev: row.value.rev,
|
||||
_deleted: true as const,
|
||||
}));
|
||||
|
||||
if (deletions.length > 0) {
|
||||
await this.raw.bulkDocs(deletions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel any active sync and completely destroy the local database.
|
||||
* After calling this, the wrapper instance should not be used again.
|
||||
*/
|
||||
async destroy(): Promise<void> {
|
||||
if (this.syncHandler) {
|
||||
this.syncHandler.cancel();
|
||||
}
|
||||
if (this.changesFeed) {
|
||||
this.changesFeed.cancel();
|
||||
}
|
||||
this.listeners.clear();
|
||||
await this.raw.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the live sync connection (if active) without destroying the database.
|
||||
*/
|
||||
cancelSync(): void {
|
||||
if (this.syncHandler) {
|
||||
this.syncHandler.cancel();
|
||||
this.syncHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to real-time changes in the database.
|
||||
* Returns an unsubscribe function.
|
||||
*/
|
||||
onChange(callback: () => void): () => void {
|
||||
this.listeners.add(callback);
|
||||
|
||||
if (!this.changesFeed) {
|
||||
this.changesFeed = this.raw.changes({
|
||||
since: 'now',
|
||||
live: true,
|
||||
include_docs: true
|
||||
}).on('change', () => {
|
||||
this.listeners.forEach(cb => cb());
|
||||
}).on('error', (err) => {
|
||||
console.warn(`[PouchDB Listener Error]`, err);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.listeners.delete(callback);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ─── PouchDatabaseManager ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* IoC Factory for managing multiple PouchDB database instances with optional
|
||||
* bi-directional CouchDB synchronization.
|
||||
*
|
||||
* `register()` returns a `PouchDatabaseWrapper` with full CRUD + query helpers,
|
||||
* so developers never need to pass `dbName` into each operation.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const dbManager = new PouchDatabaseManager();
|
||||
*
|
||||
* const itemDB = dbManager.register({ localName: 'items' });
|
||||
* await itemDB.create({ _id: 'item-001', name: 'Widget', price: 9.99 });
|
||||
*
|
||||
* const results = await itemDB.find({ selector: { price: { $gt: 5 } } });
|
||||
* console.log(results); // [{ _id: 'item-001', name: 'Widget', price: 9.99, ... }]
|
||||
*
|
||||
* const salesDB = dbManager.register({
|
||||
* localName: 'sales',
|
||||
* remoteUrl: 'http://admin:pass@localhost:5984/sales_db',
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class PouchDatabaseManager {
|
||||
private databases = new Map<string, PouchDatabaseWrapper<any>>();
|
||||
|
||||
/**
|
||||
* Register and initialize a new PouchDB database.
|
||||
* If a database with the same `localName` already exists, returns the existing wrapper.
|
||||
*
|
||||
* @param config - Configuration for the database instance.
|
||||
* @returns A `PouchDatabaseWrapper` with CRUD + query helpers.
|
||||
*/
|
||||
register<T extends object = any>(config: PouchConfig): PouchDatabaseWrapper<T> {
|
||||
if (this.databases.has(config.localName)) {
|
||||
return this.databases.get(config.localName) as PouchDatabaseWrapper<T>;
|
||||
}
|
||||
|
||||
const wrapper = new PouchDatabaseWrapper<T>(config);
|
||||
this.databases.set(config.localName, wrapper);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a previously registered database wrapper by its local name.
|
||||
*/
|
||||
get<T extends object = any>(localName: string): PouchDatabaseWrapper<T> | undefined {
|
||||
return this.databases.get(localName) as PouchDatabaseWrapper<T> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a specific registered database and remove it from the manager.
|
||||
*/
|
||||
async destroy(localName: string): Promise<void> {
|
||||
const wrapper = this.databases.get(localName);
|
||||
if (!wrapper) return;
|
||||
await wrapper.destroy();
|
||||
this.databases.delete(localName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy all managed databases. Useful for logout/cleanup scenarios.
|
||||
*/
|
||||
async destroyAll(): Promise<void> {
|
||||
const names = Array.from(this.databases.keys());
|
||||
await Promise.all(names.map((name) => this.destroy(name)));
|
||||
}
|
||||
|
||||
/**
|
||||
* List all currently registered database names.
|
||||
*/
|
||||
listDatabases(): string[] {
|
||||
return Array.from(this.databases.keys());
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ describe('LocalStorageService', () => {
|
||||
});
|
||||
|
||||
it('returns null for non-existent keys', async () => {
|
||||
const result = await storage.getItem<string>('nonexistent' as TestStorageKeyValue);
|
||||
const result = await storage.getItem<string>(TestStorageKey.THEME);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import PouchDB from 'pouchdb-core';
|
||||
import PouchDBAdapterMemory from 'pouchdb-adapter-memory';
|
||||
import PouchDBFind from 'pouchdb-find';
|
||||
import PouchDBMapReduce from 'pouchdb-mapreduce';
|
||||
|
||||
// Build a minimal PouchDB for testing: core + memory adapter + find
|
||||
PouchDB.plugin(PouchDBAdapterMemory);
|
||||
PouchDB.plugin(PouchDBFind);
|
||||
PouchDB.plugin(PouchDBMapReduce);
|
||||
|
||||
/**
|
||||
* Since the tests run in Node (not a browser), we cannot use PouchDatabaseManager
|
||||
* directly because it imports `pouchdb-browser` which requires `self`.
|
||||
* Instead, we test the CRUD logic by creating a lightweight test wrapper
|
||||
* that mirrors PouchDatabaseWrapper's methods using the memory-backed PouchDB.
|
||||
*/
|
||||
|
||||
function createTestDB(name: string) {
|
||||
const raw = new PouchDB(name, { adapter: 'memory' });
|
||||
|
||||
return {
|
||||
raw,
|
||||
|
||||
async create<T extends object>(data: T) {
|
||||
return raw.put(data as PouchDB.Core.Document<T>);
|
||||
},
|
||||
|
||||
async update<T extends object>(id: string, data: Partial<T>) {
|
||||
const existing = await raw.get(id);
|
||||
const merged = { ...existing, ...data };
|
||||
return raw.put(merged);
|
||||
},
|
||||
|
||||
async delete(id: string) {
|
||||
const doc = await raw.get(id);
|
||||
return raw.remove(doc);
|
||||
},
|
||||
|
||||
async getOne<T>(id: string) {
|
||||
return raw.get<T>(id) as Promise<T & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta>;
|
||||
},
|
||||
|
||||
async getAll<T>() {
|
||||
const result = await raw.allDocs({ include_docs: true });
|
||||
return result.rows
|
||||
.filter((row) => !row.id.startsWith('_design/'))
|
||||
.map((row) => row.doc as unknown as T);
|
||||
},
|
||||
|
||||
async getSome<T>(ids: string[]) {
|
||||
const result = await raw.allDocs({ keys: ids, include_docs: true });
|
||||
return result.rows
|
||||
.filter((row): row is any => !('error' in row) && !!(row as any).doc)
|
||||
.map((row: any) => row.doc as T);
|
||||
},
|
||||
|
||||
async find<T extends object>(options: PouchDB.Find.FindRequest<T>) {
|
||||
const result = await raw.find(options as PouchDB.Find.FindRequest<object>);
|
||||
return result.docs as unknown as T[];
|
||||
},
|
||||
|
||||
async cleanAllData() {
|
||||
const result = await raw.allDocs();
|
||||
const deletions = result.rows
|
||||
.filter((row) => !row.id.startsWith('_design/'))
|
||||
.map((row) => ({
|
||||
_id: row.id,
|
||||
_rev: row.value.rev,
|
||||
_deleted: true as const,
|
||||
}));
|
||||
if (deletions.length > 0) {
|
||||
await raw.bulkDocs(deletions);
|
||||
}
|
||||
},
|
||||
|
||||
async destroy() {
|
||||
await raw.destroy();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('PouchDatabaseWrapper CRUD Operations', () => {
|
||||
let db: ReturnType<typeof createTestDB>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Use a unique name per test to avoid cross-contamination
|
||||
db = createTestDB(`test_db_${Date.now()}_${Math.random().toString(36).slice(2)}`);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await db.destroy();
|
||||
} catch {
|
||||
// Already destroyed in some tests
|
||||
}
|
||||
});
|
||||
|
||||
// ─── create ───────────────────────────────────────────────────
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a document with a given _id', async () => {
|
||||
const res = await db.create({ _id: 'doc-001', name: 'Alice', age: 30 });
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.id).toBe('doc-001');
|
||||
});
|
||||
|
||||
it('should throw a conflict if creating with a duplicate _id', async () => {
|
||||
await db.create({ _id: 'dup-001', name: 'First' });
|
||||
await expect(db.create({ _id: 'dup-001', name: 'Second' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getOne ───────────────────────────────────────────────────
|
||||
|
||||
describe('getOne', () => {
|
||||
it('should retrieve a document by id', async () => {
|
||||
await db.create({ _id: 'fetch-001', product: 'Widget', price: 9.99 });
|
||||
|
||||
const doc = await db.getOne<{ product: string; price: number }>('fetch-001');
|
||||
expect(doc._id).toBe('fetch-001');
|
||||
expect(doc.product).toBe('Widget');
|
||||
expect(doc.price).toBe(9.99);
|
||||
});
|
||||
|
||||
it('should throw for a non-existent document', async () => {
|
||||
await expect(db.getOne('non-existent')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── update ───────────────────────────────────────────────────
|
||||
|
||||
describe('update', () => {
|
||||
it('should merge new fields into an existing document', async () => {
|
||||
await db.create({ _id: 'upd-001', name: 'Original', count: 1 });
|
||||
|
||||
const res = await db.update('upd-001', { count: 42, extra: 'field' });
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const updated = await db.getOne<{ name: string; count: number; extra: string }>('upd-001');
|
||||
expect(updated.name).toBe('Original'); // untouched
|
||||
expect(updated.count).toBe(42); // updated
|
||||
expect(updated.extra).toBe('field'); // newly added
|
||||
});
|
||||
|
||||
it('should throw when updating a non-existent document', async () => {
|
||||
await expect(db.update('ghost', { name: 'nope' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── delete ───────────────────────────────────────────────────
|
||||
|
||||
describe('delete', () => {
|
||||
it('should remove a document by id', async () => {
|
||||
await db.create({ _id: 'del-001', name: 'ToBeDeleted' });
|
||||
|
||||
const res = await db.delete('del-001');
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
await expect(db.getOne('del-001')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getAll ───────────────────────────────────────────────────
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should return all documents as a flat array', async () => {
|
||||
await db.create({ _id: 'a', val: 1 });
|
||||
await db.create({ _id: 'b', val: 2 });
|
||||
await db.create({ _id: 'c', val: 3 });
|
||||
|
||||
const all = await db.getAll<{ val: number }>();
|
||||
expect(all).toHaveLength(3);
|
||||
expect(all.map((d: any) => d.val).sort()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should return empty array for empty database', async () => {
|
||||
const all = await db.getAll();
|
||||
expect(all).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getSome ──────────────────────────────────────────────────
|
||||
|
||||
describe('getSome', () => {
|
||||
it('should return only the requested documents', async () => {
|
||||
await db.create({ _id: 'x1', v: 10 });
|
||||
await db.create({ _id: 'x2', v: 20 });
|
||||
await db.create({ _id: 'x3', v: 30 });
|
||||
|
||||
const some = await db.getSome<{ v: number }>(['x1', 'x3']);
|
||||
expect(some).toHaveLength(2);
|
||||
expect(some.map((d: any) => d.v).sort()).toEqual([10, 30]);
|
||||
});
|
||||
|
||||
it('should silently skip missing ids', async () => {
|
||||
await db.create({ _id: 'exists', v: 1 });
|
||||
|
||||
const some = await db.getSome<{ v: number }>(['exists', 'ghost']);
|
||||
expect(some).toHaveLength(1);
|
||||
expect((some[0] as any).v).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── find (pouchdb-find selectors) ────────────────────────────
|
||||
|
||||
describe('find', () => {
|
||||
it('should filter documents using selectors', async () => {
|
||||
await db.create({ _id: 'p1', category: 'electronics', price: 100 });
|
||||
await db.create({ _id: 'p2', category: 'clothing', price: 50 });
|
||||
await db.create({ _id: 'p3', category: 'electronics', price: 200 });
|
||||
|
||||
const results = await db.find<{ category: string; price: number }>({
|
||||
selector: { category: 'electronics' },
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.every((r) => r.category === 'electronics')).toBe(true);
|
||||
});
|
||||
|
||||
it('should support $gt comparisons', async () => {
|
||||
await db.create({ _id: 'i1', price: 10 });
|
||||
await db.create({ _id: 'i2', price: 50 });
|
||||
await db.create({ _id: 'i3', price: 100 });
|
||||
|
||||
const results = await db.find<{ price: number }>({
|
||||
selector: { price: { $gt: 40 } },
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.every((r) => r.price > 40)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── cleanAllData ─────────────────────────────────────────────
|
||||
|
||||
describe('cleanAllData', () => {
|
||||
it('should remove all documents but keep the database intact', async () => {
|
||||
await db.create({ _id: 'c1', name: 'One' });
|
||||
await db.create({ _id: 'c2', name: 'Two' });
|
||||
await db.create({ _id: 'c3', name: 'Three' });
|
||||
|
||||
let all = await db.getAll();
|
||||
expect(all).toHaveLength(3);
|
||||
|
||||
await db.cleanAllData();
|
||||
|
||||
all = await db.getAll();
|
||||
expect(all).toHaveLength(0);
|
||||
|
||||
// Database should still be functional after cleaning
|
||||
await db.create({ _id: 'c4', name: 'Four' });
|
||||
all = await db.getAll();
|
||||
expect(all).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Generated
+561
-1
@@ -185,9 +185,15 @@ importers:
|
||||
dayjs:
|
||||
specifier: ^1.11.19
|
||||
version: 1.11.19
|
||||
events:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
i18next:
|
||||
specifier: ^24.2.2
|
||||
version: 24.2.3(typescript@5.5.4)
|
||||
lucide-react:
|
||||
specifier: ^1.17.0
|
||||
version: 1.17.0(react@19.2.3)
|
||||
react:
|
||||
specifier: ^19.2.3
|
||||
version: 19.2.3
|
||||
@@ -379,6 +385,12 @@ importers:
|
||||
'@repo/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../utils
|
||||
pouchdb-browser:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
pouchdb-find:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
devDependencies:
|
||||
'@repo/eslint-config':
|
||||
specifier: workspace:*
|
||||
@@ -386,9 +398,36 @@ importers:
|
||||
'@repo/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../configs/typescript
|
||||
'@types/pouchdb':
|
||||
specifier: ^6.4.2
|
||||
version: 6.4.2
|
||||
'@types/pouchdb-adapter-memory':
|
||||
specifier: ^6.1.6
|
||||
version: 6.1.6
|
||||
'@types/pouchdb-browser':
|
||||
specifier: ^6.1.5
|
||||
version: 6.1.5
|
||||
'@types/pouchdb-core':
|
||||
specifier: ^7.0.15
|
||||
version: 7.0.15
|
||||
'@types/pouchdb-find':
|
||||
specifier: ^7.3.3
|
||||
version: 7.3.3
|
||||
'@types/pouchdb-mapreduce':
|
||||
specifier: ^6.1.10
|
||||
version: 6.1.10
|
||||
eslint:
|
||||
specifier: ^8.57.1
|
||||
version: 8.57.1
|
||||
pouchdb-adapter-memory:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
pouchdb-core:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
pouchdb-mapreduce:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
typescript:
|
||||
specifier: 5.5.4
|
||||
version: 5.5.4
|
||||
@@ -3125,6 +3164,135 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@types/pouchdb-adapter-cordova-sqlite@1.0.4:
|
||||
resolution: {integrity: sha512-1MGjmAMux3OIyJ+iXfhJ5hNIzS+KjGJ05O3bF5Gen5TiJUFNK1bOp3VVV9SxXgz+hGwnBruBAWdAqhbB6ZHhSA==}
|
||||
dependencies:
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-adapter-fruitdown@6.1.6:
|
||||
resolution: {integrity: sha512-KaFB29hUI97eTtJI6pjv7EQcqhZ63qHWovKgyiE+HZF5fVmdrBbTmnIrbR87AJXcXKy47+oQFJ7rzxY8TalpLQ==}
|
||||
dependencies:
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-adapter-http@6.1.6:
|
||||
resolution: {integrity: sha512-DJur1mt07GJXwGb5K+MOILoCOSgoQpsi7hybcTzRLeR3IO8Y8eq7TnhTkftAJdx9VHJGOiOXFjO+8BYM69j5yA==}
|
||||
dependencies:
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-adapter-idb@6.1.7:
|
||||
resolution: {integrity: sha512-KwjkJ4fTNz5wPXYu20bUoWud7ty0t7tgdo4oc0AJvG+fcURAH7mI7uFmpE4dZIT+hUq5G61xu96AVq9b2q4T3g==}
|
||||
dependencies:
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-adapter-leveldb@6.1.6:
|
||||
resolution: {integrity: sha512-mqeTpA2Ni2U4FA5ISRESy4WwhfUahXViUa3jQpXGdSpruaeHlhTLzZJPyz7/mGlvdAfAFv9Vd5d6ys3ASmMujw==}
|
||||
dependencies:
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-adapter-localstorage@6.1.6:
|
||||
resolution: {integrity: sha512-+HQBCpD80XkKJE64r7uLwzkNRgkvMnhDI5rIFLx3USxdrRph/R3awcEubRFndcgtxzcUaL9iYw9KetgFMUqPrg==}
|
||||
dependencies:
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-adapter-memory@6.1.6:
|
||||
resolution: {integrity: sha512-QCCtW561XuwFACzP/4zYySzs/a4em0EeuQdszen0YOaGV1/fRqJE0dOlmzh8do4sNJomLO6+MFtEzguGljnkgA==}
|
||||
dependencies:
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-adapter-node-websql@6.1.5:
|
||||
resolution: {integrity: sha512-yi68syUvHs4OM3mzKlh4zfpov64KITIAnxi387zgdby6SEfAJzWPC0dfH77iEVRDGCrKb3cKTNkl/UGHnphaow==}
|
||||
dependencies:
|
||||
'@types/pouchdb-adapter-websql': 6.1.7
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-adapter-websql@6.1.7:
|
||||
resolution: {integrity: sha512-9oNkP5ZCGMkQALO9KmtbHXlkBq8i2hoCEE6/gWzRicAvL1y+WIKjEQiIIEamMhj5u5tARvW3n2/r+JXwLCyYgw==}
|
||||
dependencies:
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-browser@6.1.5:
|
||||
resolution: {integrity: sha512-f+HjxEjYFpgoYWXnMI9AQZZ+SIG8dBiBPrpfWWGsCl+48rumsP5BuBWHq/aXoB8SRKYO0XdP4TNvMBWM3UATCw==}
|
||||
dependencies:
|
||||
'@types/pouchdb-adapter-http': 6.1.6
|
||||
'@types/pouchdb-adapter-idb': 6.1.7
|
||||
'@types/pouchdb-adapter-websql': 6.1.7
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
'@types/pouchdb-mapreduce': 6.1.10
|
||||
'@types/pouchdb-replication': 6.4.7
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-core@7.0.15:
|
||||
resolution: {integrity: sha512-gq1Qbqn9nCaAKRRv6fRHZ4/ER+QYEwSXBZlDQcxwdbPrtZO8EhIn2Bct0AlguaSEdFcABfbaxxyQwFINkNQ9dQ==}
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/pouchdb-find': 7.3.3
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-find@7.3.3:
|
||||
resolution: {integrity: sha512-U7zXk67s9Ar+9Pwj5kSbuMnn8zif0AOOIPy4KRFeJ/S/Tk+mNS90soj+3OV21H8xyB7WTxjvS1JLablZC6C6ow==}
|
||||
dependencies:
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-http@6.1.5:
|
||||
resolution: {integrity: sha512-9jGCAl6DUsXIl1vjuPu8tzGykAr84549P4IS0zYdrOKq5eXzQRUb/tb2hEVTmmTcYKXu2P1N55ABsdDNZvzGGA==}
|
||||
dependencies:
|
||||
'@types/pouchdb-adapter-http': 6.1.6
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-mapreduce@6.1.10:
|
||||
resolution: {integrity: sha512-AgYVqCnaA5D7cWkWyzZVuk0137N4yZsmIQTD/i3DmuMxYYoFrtWUoQu0tbA52SpTRGdL8ubQ7JFQXzA13fA6IQ==}
|
||||
dependencies:
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-node@6.1.7:
|
||||
resolution: {integrity: sha512-hryc2eCtNB3GbLcHSwU8glLaY66gDMus1AYkcIYAAxufdnK2BAy1oxaRLmnwRn1A1vG41P/t0htFD161LUnfQw==}
|
||||
dependencies:
|
||||
'@types/pouchdb-adapter-http': 6.1.6
|
||||
'@types/pouchdb-adapter-leveldb': 6.1.6
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
'@types/pouchdb-mapreduce': 6.1.10
|
||||
'@types/pouchdb-replication': 6.4.7
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb-replication@6.4.7:
|
||||
resolution: {integrity: sha512-slB4zOwri3SAVHioFx/FWC/KqOzzb7nDFtV+qzaKzxkf+U5zTwCbK3uRHaj0d/XQk0DwVeajf1ni3Wiyq3j2OA==}
|
||||
dependencies:
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
'@types/pouchdb-find': 7.3.3
|
||||
dev: true
|
||||
|
||||
/@types/pouchdb@6.4.2:
|
||||
resolution: {integrity: sha512-YsI47rASdtzR+3V3JE2UKY58snhm0AglHBpyckQBkRYoCbTvGagXHtV0x5n8nzN04jQmvTG+Sm85cIzKT3KXBA==}
|
||||
dependencies:
|
||||
'@types/pouchdb-adapter-cordova-sqlite': 1.0.4
|
||||
'@types/pouchdb-adapter-fruitdown': 6.1.6
|
||||
'@types/pouchdb-adapter-http': 6.1.6
|
||||
'@types/pouchdb-adapter-idb': 6.1.7
|
||||
'@types/pouchdb-adapter-leveldb': 6.1.6
|
||||
'@types/pouchdb-adapter-localstorage': 6.1.6
|
||||
'@types/pouchdb-adapter-memory': 6.1.6
|
||||
'@types/pouchdb-adapter-node-websql': 6.1.5
|
||||
'@types/pouchdb-adapter-websql': 6.1.7
|
||||
'@types/pouchdb-browser': 6.1.5
|
||||
'@types/pouchdb-core': 7.0.15
|
||||
'@types/pouchdb-http': 6.1.5
|
||||
'@types/pouchdb-mapreduce': 6.1.10
|
||||
'@types/pouchdb-node': 6.1.7
|
||||
'@types/pouchdb-replication': 6.4.7
|
||||
dev: true
|
||||
|
||||
/@types/react-dom@19.2.3(@types/react@19.2.7):
|
||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
||||
peerDependencies:
|
||||
@@ -3803,6 +3971,25 @@ packages:
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
dev: false
|
||||
|
||||
/abstract-leveldown@2.7.2:
|
||||
resolution: {integrity: sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w==}
|
||||
deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
dev: true
|
||||
|
||||
/abstract-leveldown@6.2.3:
|
||||
resolution: {integrity: sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==}
|
||||
engines: {node: '>=6'}
|
||||
deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
immediate: 3.3.0
|
||||
level-concat-iterator: 2.0.1
|
||||
level-supports: 1.0.1
|
||||
xtend: 4.0.2
|
||||
dev: true
|
||||
|
||||
/acorn-import-attributes@1.9.5(acorn@8.15.0):
|
||||
resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
|
||||
peerDependencies:
|
||||
@@ -4902,6 +5089,15 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/deferred-leveldown@5.3.0:
|
||||
resolution: {integrity: sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==}
|
||||
engines: {node: '>=6'}
|
||||
deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
|
||||
dependencies:
|
||||
abstract-leveldown: 6.2.3
|
||||
inherits: 2.0.4
|
||||
dev: true
|
||||
|
||||
/define-data-property@1.1.4:
|
||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5053,6 +5249,10 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/double-ended-queue@2.1.0-0:
|
||||
resolution: {integrity: sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ==}
|
||||
dev: true
|
||||
|
||||
/dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5217,6 +5417,13 @@ packages:
|
||||
/err-code@2.0.3:
|
||||
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
|
||||
|
||||
/errno@0.1.8:
|
||||
resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
prr: 1.0.1
|
||||
dev: true
|
||||
|
||||
/error-ex@1.3.4:
|
||||
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
||||
dependencies:
|
||||
@@ -5934,6 +6141,11 @@ packages:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
/events@3.3.0:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
dev: false
|
||||
|
||||
/execa@5.1.1:
|
||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -6025,6 +6237,12 @@ packages:
|
||||
dependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
/fetch-cookie@2.2.0:
|
||||
resolution: {integrity: sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==}
|
||||
dependencies:
|
||||
set-cookie-parser: 2.7.2
|
||||
tough-cookie: 4.1.4
|
||||
|
||||
/file-entry-cache@6.0.1:
|
||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
@@ -6175,6 +6393,10 @@ packages:
|
||||
is-callable: 1.2.7
|
||||
dev: false
|
||||
|
||||
/functional-red-black-tree@1.0.1:
|
||||
resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==}
|
||||
dev: true
|
||||
|
||||
/functions-have-names@1.2.3:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
dev: false
|
||||
@@ -6561,6 +6783,10 @@ packages:
|
||||
engines: {node: '>= 4'}
|
||||
dev: false
|
||||
|
||||
/immediate@3.3.0:
|
||||
resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==}
|
||||
dev: true
|
||||
|
||||
/import-fresh@3.3.1:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -6911,6 +7137,10 @@ packages:
|
||||
is-docker: 2.2.1
|
||||
dev: true
|
||||
|
||||
/isarray@0.0.1:
|
||||
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
|
||||
dev: true
|
||||
|
||||
/isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
dev: true
|
||||
@@ -7126,6 +7356,56 @@ packages:
|
||||
readable-stream: 2.3.8
|
||||
dev: true
|
||||
|
||||
/level-codec@9.0.2:
|
||||
resolution: {integrity: sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==}
|
||||
engines: {node: '>=6'}
|
||||
deprecated: Superseded by level-transcoder (https://github.com/Level/community#faq)
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
dev: true
|
||||
|
||||
/level-concat-iterator@2.0.1:
|
||||
resolution: {integrity: sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==}
|
||||
engines: {node: '>=6'}
|
||||
deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
|
||||
dev: true
|
||||
|
||||
/level-errors@2.0.1:
|
||||
resolution: {integrity: sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==}
|
||||
engines: {node: '>=6'}
|
||||
deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
|
||||
dependencies:
|
||||
errno: 0.1.8
|
||||
dev: true
|
||||
|
||||
/level-iterator-stream@4.0.2:
|
||||
resolution: {integrity: sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
xtend: 4.0.2
|
||||
dev: true
|
||||
|
||||
/level-supports@1.0.1:
|
||||
resolution: {integrity: sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
dev: true
|
||||
|
||||
/levelup@4.4.0:
|
||||
resolution: {integrity: sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==}
|
||||
engines: {node: '>=6'}
|
||||
deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
|
||||
dependencies:
|
||||
deferred-leveldown: 5.3.0
|
||||
level-errors: 2.0.1
|
||||
level-iterator-stream: 4.0.2
|
||||
level-supports: 1.0.1
|
||||
xtend: 4.0.2
|
||||
dev: true
|
||||
|
||||
/levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -7353,6 +7633,18 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/ltgt@2.2.1:
|
||||
resolution: {integrity: sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==}
|
||||
dev: true
|
||||
|
||||
/lucide-react@1.17.0(react@19.2.3):
|
||||
resolution: {integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
dependencies:
|
||||
react: 19.2.3
|
||||
dev: false
|
||||
|
||||
/lz-string@1.5.0:
|
||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||
hasBin: true
|
||||
@@ -7515,6 +7807,18 @@ packages:
|
||||
'@types/mdast': 4.0.4
|
||||
dev: false
|
||||
|
||||
/memdown@1.4.1:
|
||||
resolution: {integrity: sha512-iVrGHZB8i4OQfM155xx8akvG9FIj+ht14DX5CQkCTG4EHzZ3d3sgckIf/Lm9ivZalEsFuEVnWv2B2WZvbrro2w==}
|
||||
deprecated: Superseded by memory-level (https://github.com/Level/community#faq)
|
||||
dependencies:
|
||||
abstract-leveldown: 2.7.2
|
||||
functional-red-black-tree: 1.0.1
|
||||
immediate: 3.3.0
|
||||
inherits: 2.0.4
|
||||
ltgt: 2.2.1
|
||||
safe-buffer: 5.1.2
|
||||
dev: true
|
||||
|
||||
/memoizerific@1.11.3:
|
||||
resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
|
||||
dependencies:
|
||||
@@ -8002,6 +8306,17 @@ packages:
|
||||
semver: 7.7.1
|
||||
dev: true
|
||||
|
||||
/node-fetch@2.6.9:
|
||||
resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
/node-gyp@9.4.1:
|
||||
resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==}
|
||||
engines: {node: ^12.13 || ^14.13 || >=16}
|
||||
@@ -8447,6 +8762,163 @@ packages:
|
||||
source-map-js: 1.2.1
|
||||
dev: true
|
||||
|
||||
/pouchdb-abstract-mapreduce@9.0.0:
|
||||
resolution: {integrity: sha512-SnTtqwAEiAa3uxKbc1J7LfiBViwEkKe2xkK92zxyTXPqWBvMnh4UU3GXxx7GrXTM4L9llsQ3lSjpbH4CNqG1Mw==}
|
||||
dependencies:
|
||||
pouchdb-binary-utils: 9.0.0
|
||||
pouchdb-collate: 9.0.0
|
||||
pouchdb-errors: 9.0.0
|
||||
pouchdb-fetch: 9.0.0
|
||||
pouchdb-mapreduce-utils: 9.0.0
|
||||
pouchdb-md5: 9.0.0
|
||||
pouchdb-utils: 9.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
/pouchdb-adapter-leveldb-core@9.0.0:
|
||||
resolution: {integrity: sha512-b3ZGPtVXyivGL5SK3AIDG7PrNsZdoDpGFkmTytDTtctkVhxOg71gnXXP+CrupENPqSNG/eGbKW4w+bbMpxy6aA==}
|
||||
dependencies:
|
||||
double-ended-queue: 2.1.0-0
|
||||
levelup: 4.4.0
|
||||
pouchdb-adapter-utils: 9.0.0
|
||||
pouchdb-binary-utils: 9.0.0
|
||||
pouchdb-core: 9.0.0
|
||||
pouchdb-errors: 9.0.0
|
||||
pouchdb-json: 9.0.0
|
||||
pouchdb-md5: 9.0.0
|
||||
pouchdb-merge: 9.0.0
|
||||
pouchdb-utils: 9.0.0
|
||||
sublevel-pouchdb: 9.0.0
|
||||
through2: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/pouchdb-adapter-memory@9.0.0:
|
||||
resolution: {integrity: sha512-XbCwJ5f5U9dGdkiDikzYjTebdPHuA6Ghylx1Pq0lDe4y6l8R9xhjDSUy56pJ8G2F4Z+8QdB5FBY9EQoFlFSXWQ==}
|
||||
dependencies:
|
||||
memdown: 1.4.1
|
||||
pouchdb-adapter-leveldb-core: 9.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/pouchdb-adapter-utils@9.0.0:
|
||||
resolution: {integrity: sha512-hmbm4ey0HL0vtoY1tRTPIt2FfYjvMh3DWoGGSxXDTS73qTFQ+Fhhi5I0AnN9PcD2omfKQAVXiYks4kkMvlAHqA==}
|
||||
dependencies:
|
||||
pouchdb-binary-utils: 9.0.0
|
||||
pouchdb-errors: 9.0.0
|
||||
pouchdb-md5: 9.0.0
|
||||
pouchdb-merge: 9.0.0
|
||||
pouchdb-utils: 9.0.0
|
||||
dev: true
|
||||
|
||||
/pouchdb-binary-utils@9.0.0:
|
||||
resolution: {integrity: sha512-2OMtgDZi82vqs+zNDE0YiYjOaWkYCUcZJZKK3WkRr+XYRu+2B7umJrnygJFhUwoGedBbHSrlQBLhdNV3F1AX1A==}
|
||||
|
||||
/pouchdb-browser@9.0.0:
|
||||
resolution: {integrity: sha512-0uKFWhsTtiVOF0+aGo7mvtCTP40f6dlsLNmJUvc/lwjsX1C3v+eBfVbvykyxpFl7UTAoJkXl+g/GOzNvyMtV1g==}
|
||||
dependencies:
|
||||
spark-md5: 3.0.2
|
||||
uuid: 8.3.2
|
||||
vuvuzela: 1.0.3
|
||||
dev: false
|
||||
|
||||
/pouchdb-changes-filter@9.0.0:
|
||||
resolution: {integrity: sha512-ig0fo0WLgIjAniFJ19Uw1Y+oxiypqC+Skhd8BCETRVXOhLBzueRwEQR4thffyo0UayYVqldJfSR5wHSDvEVk/A==}
|
||||
dependencies:
|
||||
pouchdb-errors: 9.0.0
|
||||
pouchdb-selector-core: 9.0.0
|
||||
pouchdb-utils: 9.0.0
|
||||
dev: true
|
||||
|
||||
/pouchdb-collate@9.0.0:
|
||||
resolution: {integrity: sha512-TrnEDNZEmIIl+W3xKUO8h+geqVLQ90oZe5ujPkl8myUzpREULWXWQBnV5EzPXVEKDBpJlb8T3I6oy/zdWGQpdA==}
|
||||
|
||||
/pouchdb-core@9.0.0:
|
||||
resolution: {integrity: sha512-98SJgs8bqXhr4gMGuOTR8yVeLlMYy797zlOtdlvlXIxIicvocyA8ColhVVhdBXPNOGxT2HwReIMywdIVAgibpg==}
|
||||
dependencies:
|
||||
pouchdb-changes-filter: 9.0.0
|
||||
pouchdb-errors: 9.0.0
|
||||
pouchdb-fetch: 9.0.0
|
||||
pouchdb-merge: 9.0.0
|
||||
pouchdb-utils: 9.0.0
|
||||
uuid: 8.3.2
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/pouchdb-errors@9.0.0:
|
||||
resolution: {integrity: sha512-961PSMLhW0UqqdJ566g+CdLZ5pkBJRd6l4WWpCDdD0USvE4xYfYGzv43w7nZZBw1k3Xdy092yqPge7yX/tfnyw==}
|
||||
|
||||
/pouchdb-fetch@9.0.0:
|
||||
resolution: {integrity: sha512-TbE3cUcAJQrwb9kr44tDP0X+NAbcqgjsTvcL30L4xzBNJeCPTIRjukYX80s154SHJUXBxcWRiPsMmNqpXsjfCA==}
|
||||
dependencies:
|
||||
fetch-cookie: 2.2.0
|
||||
node-fetch: 2.6.9
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
/pouchdb-find@9.0.0:
|
||||
resolution: {integrity: sha512-vvVhq4eEOmSkwSRwf2NBYtdhURB7ryJ7sUI4WDN00GuLUj2g8jAXBJuZIryVgdYt/5S5cfn70iRL6Eow+LFhpA==}
|
||||
dependencies:
|
||||
pouchdb-abstract-mapreduce: 9.0.0
|
||||
pouchdb-collate: 9.0.0
|
||||
pouchdb-errors: 9.0.0
|
||||
pouchdb-fetch: 9.0.0
|
||||
pouchdb-md5: 9.0.0
|
||||
pouchdb-selector-core: 9.0.0
|
||||
pouchdb-utils: 9.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: false
|
||||
|
||||
/pouchdb-json@9.0.0:
|
||||
resolution: {integrity: sha512-aI41mYVyI195GXuT1Ys7mLIB/Mvrz11ihoTP6km6hYqVgSuaUxuZcFUozlyTJiZXr7H5kdhNgclhlVnjir4JAA==}
|
||||
dependencies:
|
||||
vuvuzela: 1.0.3
|
||||
dev: true
|
||||
|
||||
/pouchdb-mapreduce-utils@9.0.0:
|
||||
resolution: {integrity: sha512-Bjh8W6QXqp1j7MKmHhYYp5cYlcQsm5drD8Jd/F+ZlfNt18uiD2SQXWzGM5797+tiW/LszFGb8ttw0uHWjxufCQ==}
|
||||
dependencies:
|
||||
pouchdb-utils: 9.0.0
|
||||
|
||||
/pouchdb-mapreduce@9.0.0:
|
||||
resolution: {integrity: sha512-ZD8PleQ9atzQAzT2LZWsvooUVEfsen5QGv/SDfci20IleCaFW2A2q7OERrqY0YWKDCCNRsWhPWPmsFvZC9K8DQ==}
|
||||
dependencies:
|
||||
pouchdb-abstract-mapreduce: 9.0.0
|
||||
pouchdb-mapreduce-utils: 9.0.0
|
||||
pouchdb-utils: 9.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/pouchdb-md5@9.0.0:
|
||||
resolution: {integrity: sha512-58xUYBvW3/s+aH0j4uOhhN8yCk0LQ254cxBzI/gbKA9PrfwHpe4zrr0L/ia5ml3A30oH1f8aTnuVMwWDkFcuww==}
|
||||
dependencies:
|
||||
pouchdb-binary-utils: 9.0.0
|
||||
spark-md5: 3.0.2
|
||||
|
||||
/pouchdb-merge@9.0.0:
|
||||
resolution: {integrity: sha512-Xh+TgOZCkGoZpI589btKf/cTiuQ5CsnPl9YpdW4h0cAPusniN6XNsR62F+/HbL9wirI6XTEPHUrk7MsQbk3S3A==}
|
||||
dependencies:
|
||||
pouchdb-utils: 9.0.0
|
||||
dev: true
|
||||
|
||||
/pouchdb-selector-core@9.0.0:
|
||||
resolution: {integrity: sha512-ZYHYsdoedwm8j5tYofz+3+uUSK8i+7tRCBb01T0OuqDQb17+w5mzjHF8Ppi160xdPUPaWCo1Un+nLWGJzkmA3g==}
|
||||
dependencies:
|
||||
pouchdb-collate: 9.0.0
|
||||
pouchdb-utils: 9.0.0
|
||||
|
||||
/pouchdb-utils@9.0.0:
|
||||
resolution: {integrity: sha512-xWZE5c+nAslgmLC8JBZbky8AYgdz7pKtv7KTSi6CD2tuQD0WyNKib0YnhZndeE84dksTeZlqlg56RQHsHoB2LQ==}
|
||||
dependencies:
|
||||
pouchdb-errors: 9.0.0
|
||||
pouchdb-md5: 9.0.0
|
||||
uuid: 8.3.2
|
||||
|
||||
/prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -8543,6 +9015,15 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/prr@1.0.1:
|
||||
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
|
||||
dev: true
|
||||
|
||||
/psl@1.15.0:
|
||||
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
/pump@3.0.4:
|
||||
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
||||
dependencies:
|
||||
@@ -8554,6 +9035,9 @@ packages:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
/querystringify@2.2.0:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
|
||||
/queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -8791,6 +9275,15 @@ packages:
|
||||
type-fest: 0.6.0
|
||||
dev: false
|
||||
|
||||
/readable-stream@1.1.14:
|
||||
resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==}
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
inherits: 2.0.4
|
||||
isarray: 0.0.1
|
||||
string_decoder: 0.10.31
|
||||
dev: true
|
||||
|
||||
/readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
dependencies:
|
||||
@@ -8933,6 +9426,9 @@ packages:
|
||||
engines: {node: '>=0.10.5'}
|
||||
dev: false
|
||||
|
||||
/requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
|
||||
/resedit@1.7.2:
|
||||
resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
@@ -9227,7 +9723,6 @@ packages:
|
||||
|
||||
/set-cookie-parser@2.7.2:
|
||||
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
|
||||
dev: false
|
||||
|
||||
/set-function-length@1.2.2:
|
||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||
@@ -9402,6 +9897,9 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/spark-md5@3.0.2:
|
||||
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
|
||||
|
||||
/spdx-correct@3.2.0:
|
||||
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
|
||||
dependencies:
|
||||
@@ -9571,6 +10069,10 @@ packages:
|
||||
es-object-atoms: 1.1.1
|
||||
dev: false
|
||||
|
||||
/string_decoder@0.10.31:
|
||||
resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
|
||||
dev: true
|
||||
|
||||
/string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
dependencies:
|
||||
@@ -9631,6 +10133,14 @@ packages:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/sublevel-pouchdb@9.0.0:
|
||||
resolution: {integrity: sha512-pX4r8+F7wuts0C81kUJ341h4bl2aRe7qV572FE8X1FMz9VkKlmi2nPD1vfeiOJXz5Y09I4MHjGULAbqvTfQZEQ==}
|
||||
dependencies:
|
||||
level-codec: 9.0.2
|
||||
ltgt: 2.2.1
|
||||
readable-stream: 1.1.14
|
||||
dev: true
|
||||
|
||||
/sumchecker@3.0.1:
|
||||
resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==}
|
||||
engines: {node: '>= 8.0'}
|
||||
@@ -9729,6 +10239,13 @@ packages:
|
||||
/text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
||||
/through2@3.0.2:
|
||||
resolution: {integrity: sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==}
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
dev: true
|
||||
|
||||
/tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
dev: true
|
||||
@@ -9787,6 +10304,15 @@ packages:
|
||||
is-number: 7.0.0
|
||||
dev: false
|
||||
|
||||
/tough-cookie@4.1.4:
|
||||
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
psl: 1.15.0
|
||||
punycode: 2.3.1
|
||||
universalify: 0.2.0
|
||||
url-parse: 1.5.10
|
||||
|
||||
/tough-cookie@5.1.2:
|
||||
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -9794,6 +10320,9 @@ packages:
|
||||
tldts: 6.1.86
|
||||
dev: true
|
||||
|
||||
/tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
/tr46@5.1.1:
|
||||
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -10137,6 +10666,10 @@ packages:
|
||||
engines: {node: '>= 4.0.0'}
|
||||
dev: true
|
||||
|
||||
/universalify@0.2.0:
|
||||
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
|
||||
/universalify@2.0.1:
|
||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -10198,6 +10731,12 @@ packages:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
/url-parse@1.5.10:
|
||||
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||
dependencies:
|
||||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
|
||||
/use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.3):
|
||||
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -10286,6 +10825,10 @@ packages:
|
||||
which-typed-array: 1.1.19
|
||||
dev: true
|
||||
|
||||
/uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
|
||||
/uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
hasBin: true
|
||||
@@ -10602,6 +11145,9 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/vuvuzela@1.0.3:
|
||||
resolution: {integrity: sha512-Tm7jR1xTzBbPW+6y1tknKiEhz04Wf/1iZkcTJjSFcpNko43+dFW6+OOeQe9taJIug3NdfUAjFKgUSyQrIKaDvQ==}
|
||||
|
||||
/w3c-xmlserializer@5.0.0:
|
||||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -10623,6 +11169,9 @@ packages:
|
||||
resolution: {integrity: sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==}
|
||||
dev: false
|
||||
|
||||
/webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
/webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -10653,6 +11202,12 @@ packages:
|
||||
webidl-conversions: 7.0.0
|
||||
dev: true
|
||||
|
||||
/whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
/which-boxed-primitive@1.1.1:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -10792,6 +11347,11 @@ packages:
|
||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||
dev: true
|
||||
|
||||
/xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
dev: true
|
||||
|
||||
/y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
Reference in New Issue
Block a user