mirror of
https://github.com/sakaljurgis/best-choice.git
synced 2026-07-08 21:47:40 +00:00
add shared
This commit is contained in:
@@ -1,35 +1,14 @@
|
|||||||
|
import type { PaginatedResponse } from '@shared/models/pagination';
|
||||||
|
import type {
|
||||||
|
ItemPrice,
|
||||||
|
PriceCondition,
|
||||||
|
PriceSourceType
|
||||||
|
} from '@shared/models/item-price';
|
||||||
import { apiFetch } from './http-client';
|
import { apiFetch } from './http-client';
|
||||||
|
|
||||||
export type PriceCondition = 'new' | 'used';
|
export type { PriceCondition, PriceSourceType };
|
||||||
export type PriceSourceType = 'url' | 'manual';
|
|
||||||
|
|
||||||
export interface ItemPrice {
|
export type ItemPricesResponse = PaginatedResponse<ItemPrice>;
|
||||||
id: string;
|
|
||||||
itemId: string;
|
|
||||||
condition: PriceCondition;
|
|
||||||
amount: number;
|
|
||||||
currency: string;
|
|
||||||
sourceType: PriceSourceType;
|
|
||||||
sourceUrlId: string | null;
|
|
||||||
sourceUrl: string | null;
|
|
||||||
sourceNote: string | null;
|
|
||||||
note: string | null;
|
|
||||||
observedAt: string;
|
|
||||||
isPrimary: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ListResponseMeta {
|
|
||||||
limit: number;
|
|
||||||
offset: number;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ItemPricesResponse {
|
|
||||||
data: ItemPrice[];
|
|
||||||
meta: ListResponseMeta;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FetchItemPricesParams {
|
export interface FetchItemPricesParams {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
|||||||
+3
-43
@@ -1,48 +1,8 @@
|
|||||||
|
import type { PaginatedResponse } from '@shared/models/pagination';
|
||||||
|
import type { Item, ItemStatus } from '@shared/models/item';
|
||||||
import { apiFetch } from './http-client';
|
import { apiFetch } from './http-client';
|
||||||
|
|
||||||
export type ItemStatus = 'active' | 'rejected';
|
export type ProjectItemsResponse = PaginatedResponse<Item>;
|
||||||
|
|
||||||
export interface Item {
|
|
||||||
id: string;
|
|
||||||
projectId: string;
|
|
||||||
manufacturer: string | null;
|
|
||||||
model: string;
|
|
||||||
sourceUrlId: string | null;
|
|
||||||
sourceUrl: string | null;
|
|
||||||
status: ItemStatus;
|
|
||||||
note: string | null;
|
|
||||||
attributes: Record<string, unknown>;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
priceSummary: ItemPriceSummary | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ItemPriceSummary {
|
|
||||||
minAmount: number;
|
|
||||||
maxAmount: number;
|
|
||||||
currency: string | null;
|
|
||||||
priceCount: number;
|
|
||||||
hasMixedCurrency: boolean;
|
|
||||||
newMinAmount: number | null;
|
|
||||||
newCount: number;
|
|
||||||
newCurrency: string | null;
|
|
||||||
newHasMixedCurrency: boolean;
|
|
||||||
usedMinAmount: number | null;
|
|
||||||
usedCount: number;
|
|
||||||
usedCurrency: string | null;
|
|
||||||
usedHasMixedCurrency: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ListResponseMeta {
|
|
||||||
limit: number;
|
|
||||||
offset: number;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProjectItemsResponse {
|
|
||||||
data: Item[];
|
|
||||||
meta: ListResponseMeta;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FetchProjectItemsParams {
|
export interface FetchProjectItemsParams {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
|||||||
@@ -1,28 +1,8 @@
|
|||||||
|
import type { PaginatedResponse } from '@shared/models/pagination';
|
||||||
|
import type { Project, ProjectStatus } from '@shared/models/project';
|
||||||
import { apiFetch } from './http-client';
|
import { apiFetch } from './http-client';
|
||||||
|
|
||||||
export type ProjectStatus = 'active' | 'archived';
|
export type ProjectsListResponse = PaginatedResponse<Project>;
|
||||||
|
|
||||||
export interface Project {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string | null;
|
|
||||||
status: ProjectStatus;
|
|
||||||
attributes: string[];
|
|
||||||
priorityRules: unknown;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ListResponseMeta {
|
|
||||||
limit: number;
|
|
||||||
offset: number;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProjectsListResponse {
|
|
||||||
data: Project[];
|
|
||||||
meta: ListResponseMeta;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FetchProjectsParams {
|
export interface FetchProjectsParams {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import type { Item } from '../../api/items';
|
import type { Item } from '@shared/models/item';
|
||||||
import { ItemPricesPanel } from './item-prices-panel';
|
import { ItemPricesPanel } from './item-prices-panel';
|
||||||
|
|
||||||
interface ItemPricesModalProps {
|
interface ItemPricesModalProps {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import type { Item } from '@shared/models/item';
|
||||||
import {
|
import {
|
||||||
useCreateItemMutation,
|
useCreateItemMutation,
|
||||||
useUpdateItemMutation,
|
useUpdateItemMutation,
|
||||||
@@ -11,7 +12,6 @@ import {
|
|||||||
importItemFromUrl,
|
importItemFromUrl,
|
||||||
type CreateItemPayload,
|
type CreateItemPayload,
|
||||||
type ImportedItemData,
|
type ImportedItemData,
|
||||||
type Item,
|
|
||||||
type UpdateItemPayload
|
type UpdateItemPayload
|
||||||
} from '../../api/items';
|
} from '../../api/items';
|
||||||
import { ItemFormModal } from '../../components/item-form-modal';
|
import { ItemFormModal } from '../../components/item-form-modal';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import type { ProjectStatus } from '../../api/projects';
|
import type { ProjectStatus } from '@shared/models/project';
|
||||||
|
|
||||||
interface ProjectHeaderProps {
|
interface ProjectHeaderProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { ExternalLink, Pencil } from 'lucide-react';
|
import { ExternalLink, Pencil } from 'lucide-react';
|
||||||
import type { Item } from '../../api/items';
|
import type { Item } from '@shared/models/item';
|
||||||
|
|
||||||
interface ProjectItemsSectionProps {
|
interface ProjectItemsSectionProps {
|
||||||
items: Item[];
|
items: Item[];
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
fetchProjects,
|
fetchProjects,
|
||||||
updateProject,
|
updateProject,
|
||||||
type CreateProjectPayload,
|
type CreateProjectPayload,
|
||||||
type Project,
|
|
||||||
type ProjectsListResponse,
|
type ProjectsListResponse,
|
||||||
type UpdateProjectPayload
|
type UpdateProjectPayload
|
||||||
} from '../api/projects';
|
} from '../api/projects';
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
type ProjectItemsResponse,
|
type ProjectItemsResponse,
|
||||||
type UpdateItemPayload
|
type UpdateItemPayload
|
||||||
} from '../api/items';
|
} from '../api/items';
|
||||||
|
import type { Project } from '@shared/models/project';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
const DEFAULT_LIMIT = 50;
|
const DEFAULT_LIMIT = 50;
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"types": ["vite/client"],
|
"types": ["vite/client"],
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"baseUrl": "./src"
|
"baseUrl": "./src",
|
||||||
|
"paths": {
|
||||||
|
"@shared/*": ["../../shared/*.d.ts"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ export default defineConfig(({ mode }) => {
|
|||||||
port: clientPort,
|
port: clientPort,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': proxyTarget
|
'/api': proxyTarget
|
||||||
|
},
|
||||||
|
fs: {
|
||||||
|
allow: [path.resolve(__dirname, '..')]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@shared': path.resolve(__dirname, '../shared')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+10
@@ -27,6 +27,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.90.2",
|
"@tanstack/react-query": "^5.90.2",
|
||||||
|
"lucide-react": "^0.545.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.9.4"
|
"react-router-dom": "^7.9.4"
|
||||||
@@ -3926,6 +3927,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.545.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz",
|
||||||
|
"integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ItemPrice, PriceCondition, PriceSourceType } from '@shared/models/item-price';
|
||||||
import { pool, query } from './pool.js';
|
import { pool, query } from './pool.js';
|
||||||
|
|
||||||
const priceColumns = `
|
const priceColumns = `
|
||||||
@@ -20,10 +21,10 @@ const priceColumns = `
|
|||||||
export interface ItemPriceRow {
|
export interface ItemPriceRow {
|
||||||
id: string;
|
id: string;
|
||||||
item_id: string;
|
item_id: string;
|
||||||
condition: 'new' | 'used';
|
condition: PriceCondition;
|
||||||
amount: string;
|
amount: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
source_type: 'url' | 'manual';
|
source_type: PriceSourceType;
|
||||||
source_url_id: string | null;
|
source_url_id: string | null;
|
||||||
source_note: string | null;
|
source_note: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
@@ -34,22 +35,7 @@ export interface ItemPriceRow {
|
|||||||
source_url: string | null;
|
source_url: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ItemPriceRecord {
|
export type ItemPriceRecord = ItemPrice;
|
||||||
id: string;
|
|
||||||
itemId: string;
|
|
||||||
condition: 'new' | 'used';
|
|
||||||
amount: number;
|
|
||||||
currency: string;
|
|
||||||
sourceType: 'url' | 'manual';
|
|
||||||
sourceUrlId: string | null;
|
|
||||||
sourceUrl: string | null;
|
|
||||||
sourceNote: string | null;
|
|
||||||
note: string | null;
|
|
||||||
observedAt: string;
|
|
||||||
isPrimary: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapItemPriceRow = (row: ItemPriceRow): ItemPriceRecord => ({
|
const mapItemPriceRow = (row: ItemPriceRow): ItemPriceRecord => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -72,7 +58,7 @@ export interface ListItemPricesOptions {
|
|||||||
itemId: string;
|
itemId: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
condition?: 'new' | 'used';
|
condition?: PriceCondition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const listItemPrices = async (
|
export const listItemPrices = async (
|
||||||
@@ -109,10 +95,10 @@ export const listItemPrices = async (
|
|||||||
|
|
||||||
export interface CreateItemPriceParams {
|
export interface CreateItemPriceParams {
|
||||||
itemId: string;
|
itemId: string;
|
||||||
condition: 'new' | 'used';
|
condition: PriceCondition;
|
||||||
amount: number;
|
amount: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
sourceType: 'url' | 'manual';
|
sourceType: PriceSourceType;
|
||||||
sourceUrlId: string | null;
|
sourceUrlId: string | null;
|
||||||
sourceNote: string | null;
|
sourceNote: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
@@ -213,10 +199,10 @@ export const getItemPriceById = async (id: string): Promise<ItemPriceRecord | nu
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface UpdateItemPriceParams {
|
export interface UpdateItemPriceParams {
|
||||||
condition?: 'new' | 'used';
|
condition?: PriceCondition;
|
||||||
amount?: number;
|
amount?: number;
|
||||||
currency?: string;
|
currency?: string;
|
||||||
sourceType?: 'url' | 'manual';
|
sourceType?: PriceSourceType;
|
||||||
sourceUrlId?: string | null;
|
sourceUrlId?: string | null;
|
||||||
sourceNote?: string | null;
|
sourceNote?: string | null;
|
||||||
note?: string | null;
|
note?: string | null;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Item, ItemPriceSummary, ItemStatus } from '@shared/models/item';
|
||||||
import { query } from './pool.js';
|
import { query } from './pool.js';
|
||||||
|
|
||||||
const itemColumns = `
|
const itemColumns = `
|
||||||
@@ -33,7 +34,7 @@ export interface ItemRow {
|
|||||||
manufacturer: string | null;
|
manufacturer: string | null;
|
||||||
model: string;
|
model: string;
|
||||||
source_url_id: string | null;
|
source_url_id: string | null;
|
||||||
status: 'active' | 'rejected';
|
status: ItemStatus;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
attributes: Record<string, unknown>;
|
attributes: Record<string, unknown>;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
@@ -54,36 +55,7 @@ export interface ItemRow {
|
|||||||
price_used_currency_count: number | null;
|
price_used_currency_count: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ItemRecord {
|
export type ItemRecord = Item;
|
||||||
id: string;
|
|
||||||
projectId: string;
|
|
||||||
manufacturer: string | null;
|
|
||||||
model: string;
|
|
||||||
sourceUrlId: string | null;
|
|
||||||
sourceUrl: string | null;
|
|
||||||
status: 'active' | 'rejected';
|
|
||||||
note: string | null;
|
|
||||||
attributes: Record<string, unknown>;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
priceSummary: ItemPriceSummary | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ItemPriceSummary {
|
|
||||||
minAmount: number;
|
|
||||||
maxAmount: number;
|
|
||||||
currency: string | null;
|
|
||||||
priceCount: number;
|
|
||||||
hasMixedCurrency: boolean;
|
|
||||||
newMinAmount: number | null;
|
|
||||||
newCount: number;
|
|
||||||
newCurrency: string | null;
|
|
||||||
newHasMixedCurrency: boolean;
|
|
||||||
usedMinAmount: number | null;
|
|
||||||
usedCount: number;
|
|
||||||
usedCurrency: string | null;
|
|
||||||
usedHasMixedCurrency: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapItemRow = (row: ItemRow): ItemRecord => {
|
const mapItemRow = (row: ItemRow): ItemRecord => {
|
||||||
const priceCount = row.price_count ?? 0;
|
const priceCount = row.price_count ?? 0;
|
||||||
@@ -126,7 +98,7 @@ const mapItemRow = (row: ItemRow): ItemRecord => {
|
|||||||
|
|
||||||
export interface ListItemsOptions {
|
export interface ListItemsOptions {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
status?: 'active' | 'rejected';
|
status?: ItemStatus;
|
||||||
limit: number;
|
limit: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
}
|
}
|
||||||
@@ -184,7 +156,7 @@ export interface CreateItemParams {
|
|||||||
manufacturer: string | null;
|
manufacturer: string | null;
|
||||||
model: string;
|
model: string;
|
||||||
sourceUrlId: string | null;
|
sourceUrlId: string | null;
|
||||||
status: 'active' | 'rejected';
|
status: ItemStatus;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
attributes: Record<string, unknown>;
|
attributes: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
@@ -260,7 +232,7 @@ export interface UpdateItemParams {
|
|||||||
manufacturer?: string | null;
|
manufacturer?: string | null;
|
||||||
model?: string;
|
model?: string;
|
||||||
sourceUrlId?: string | null;
|
sourceUrlId?: string | null;
|
||||||
status?: 'active' | 'rejected';
|
status?: ItemStatus;
|
||||||
note?: string | null;
|
note?: string | null;
|
||||||
attributes?: Record<string, unknown>;
|
attributes?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Project, ProjectStatus } from '@shared/models/project';
|
||||||
import { query } from './pool.js';
|
import { query } from './pool.js';
|
||||||
|
|
||||||
const projectColumns = `
|
const projectColumns = `
|
||||||
@@ -15,24 +16,13 @@ export interface ProjectRow {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
status: 'active' | 'archived';
|
status: ProjectStatus;
|
||||||
project_attributes: string[];
|
project_attributes: string[];
|
||||||
project_priority_rules: unknown;
|
project_priority_rules: unknown;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Project {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string | null;
|
|
||||||
status: 'active' | 'archived';
|
|
||||||
attributes: string[];
|
|
||||||
priorityRules: unknown;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapProjectRow = (row: ProjectRow): Project => ({
|
const mapProjectRow = (row: ProjectRow): Project => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
@@ -47,7 +37,7 @@ const mapProjectRow = (row: ProjectRow): Project => ({
|
|||||||
export interface ListProjectsOptions {
|
export interface ListProjectsOptions {
|
||||||
limit: number;
|
limit: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
status?: 'active' | 'archived';
|
status?: ProjectStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const listProjects = async (
|
export const listProjects = async (
|
||||||
@@ -86,7 +76,7 @@ export const listProjects = async (
|
|||||||
export interface CreateProjectParams {
|
export interface CreateProjectParams {
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
status: 'active' | 'archived';
|
status: ProjectStatus;
|
||||||
attributes: string[];
|
attributes: string[];
|
||||||
priorityRules: unknown;
|
priorityRules: unknown;
|
||||||
}
|
}
|
||||||
@@ -136,7 +126,7 @@ export const getProjectById = async (id: string): Promise<Project | null> => {
|
|||||||
export interface UpdateProjectParams {
|
export interface UpdateProjectParams {
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
status?: 'active' | 'archived';
|
status?: ProjectStatus;
|
||||||
attributes?: string[];
|
attributes?: string[];
|
||||||
priorityRules?: unknown;
|
priorityRules?: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
|
import type { PriceCondition, PriceSourceType } from '@shared/models/item-price';
|
||||||
import { HttpError } from '../errors/http-error.js';
|
import { HttpError } from '../errors/http-error.js';
|
||||||
import { parseUuid } from './common.js';
|
import { parseUuid } from './common.js';
|
||||||
|
|
||||||
const priceConditions = new Set(['new', 'used']);
|
const priceConditions = new Set<PriceCondition>(['new', 'used']);
|
||||||
const priceSources = new Set(['url', 'manual']);
|
const priceSources = new Set<PriceSourceType>(['url', 'manual']);
|
||||||
|
|
||||||
|
const isPriceCondition = (value: unknown): value is PriceCondition => {
|
||||||
|
return typeof value === 'string' && priceConditions.has(value as PriceCondition);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPriceSourceType = (value: unknown): value is PriceSourceType => {
|
||||||
|
return typeof value === 'string' && priceSources.has(value as PriceSourceType);
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeCurrency = (value: unknown): string => {
|
const normalizeCurrency = (value: unknown): string => {
|
||||||
if (typeof value !== 'string' || value.trim().length !== 3) {
|
if (typeof value !== 'string' || value.trim().length !== 3) {
|
||||||
@@ -29,10 +38,10 @@ const parseObservedAt = (value: unknown): string | undefined => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface ItemPriceCreateInput {
|
export interface ItemPriceCreateInput {
|
||||||
condition: 'new' | 'used';
|
condition: PriceCondition;
|
||||||
amount: number;
|
amount: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
sourceType: 'url' | 'manual';
|
sourceType: PriceSourceType;
|
||||||
sourceUrlId: string | null;
|
sourceUrlId: string | null;
|
||||||
sourceUrl: string | null;
|
sourceUrl: string | null;
|
||||||
sourceNote: string | null;
|
sourceNote: string | null;
|
||||||
@@ -81,7 +90,7 @@ const parseIsPrimary = (value: unknown): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const parseSourceFields = (
|
const parseSourceFields = (
|
||||||
sourceType: 'url' | 'manual',
|
sourceType: PriceSourceType,
|
||||||
sourceUrlIdValue: unknown,
|
sourceUrlIdValue: unknown,
|
||||||
sourceUrlValue: unknown
|
sourceUrlValue: unknown
|
||||||
): { sourceUrlId: string | null; sourceUrl: string | null } => {
|
): { sourceUrlId: string | null; sourceUrl: string | null } => {
|
||||||
@@ -144,25 +153,25 @@ export const parseItemPriceCreatePayload = (
|
|||||||
isPrimary
|
isPrimary
|
||||||
} = payload as Record<string, unknown>;
|
} = payload as Record<string, unknown>;
|
||||||
|
|
||||||
if (typeof condition !== 'string' || !priceConditions.has(condition)) {
|
if (!isPriceCondition(condition)) {
|
||||||
throw new HttpError(400, 'condition must be one of new, used');
|
throw new HttpError(400, 'condition must be one of new, used');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof sourceType !== 'string' || !priceSources.has(sourceType)) {
|
if (!isPriceSourceType(sourceType)) {
|
||||||
throw new HttpError(400, 'sourceType must be one of url, manual');
|
throw new HttpError(400, 'sourceType must be one of url, manual');
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = parseSourceFields(
|
const source = parseSourceFields(
|
||||||
sourceType as 'url' | 'manual',
|
sourceType,
|
||||||
sourceUrlId,
|
sourceUrlId,
|
||||||
sourceUrl
|
sourceUrl
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
condition: condition as 'new' | 'used',
|
condition,
|
||||||
amount: parseAmount(amount),
|
amount: parseAmount(amount),
|
||||||
currency: normalizeCurrency(currency),
|
currency: normalizeCurrency(currency),
|
||||||
sourceType: sourceType as 'url' | 'manual',
|
sourceType,
|
||||||
sourceUrlId: source.sourceUrlId,
|
sourceUrlId: source.sourceUrlId,
|
||||||
sourceUrl: source.sourceUrl,
|
sourceUrl: source.sourceUrl,
|
||||||
sourceNote: parseSourceNote(sourceNote),
|
sourceNote: parseSourceNote(sourceNote),
|
||||||
@@ -194,10 +203,10 @@ export const parseItemPriceUpdatePayload = (
|
|||||||
} = payload as Record<string, unknown>;
|
} = payload as Record<string, unknown>;
|
||||||
|
|
||||||
if (condition !== undefined) {
|
if (condition !== undefined) {
|
||||||
if (typeof condition !== 'string' || !priceConditions.has(condition)) {
|
if (!isPriceCondition(condition)) {
|
||||||
throw new HttpError(400, 'condition must be one of new, used');
|
throw new HttpError(400, 'condition must be one of new, used');
|
||||||
}
|
}
|
||||||
result.condition = condition as 'new' | 'used';
|
result.condition = condition;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (amount !== undefined) {
|
if (amount !== undefined) {
|
||||||
@@ -209,14 +218,14 @@ export const parseItemPriceUpdatePayload = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sourceType !== undefined) {
|
if (sourceType !== undefined) {
|
||||||
if (typeof sourceType !== 'string' || !priceSources.has(sourceType)) {
|
if (!isPriceSourceType(sourceType)) {
|
||||||
throw new HttpError(400, 'sourceType must be one of url, manual');
|
throw new HttpError(400, 'sourceType must be one of url, manual');
|
||||||
}
|
}
|
||||||
result.sourceType = sourceType as 'url' | 'manual';
|
result.sourceType = sourceType;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceUrlId !== undefined || sourceUrl !== undefined) {
|
if (sourceUrlId !== undefined || sourceUrl !== undefined) {
|
||||||
const type = result.sourceType ?? (sourceType as 'url' | 'manual' | undefined);
|
const type = result.sourceType ?? (isPriceSourceType(sourceType) ? sourceType : undefined);
|
||||||
if (!type) {
|
if (!type) {
|
||||||
throw new HttpError(
|
throw new HttpError(
|
||||||
400,
|
400,
|
||||||
@@ -253,12 +262,12 @@ export const parseItemPriceUpdatePayload = (
|
|||||||
|
|
||||||
export const parsePriceConditionFilter = (
|
export const parsePriceConditionFilter = (
|
||||||
value: unknown
|
value: unknown
|
||||||
): 'new' | 'used' | undefined => {
|
): PriceCondition | undefined => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (typeof value !== 'string' || !priceConditions.has(value)) {
|
if (!isPriceCondition(value)) {
|
||||||
throw new HttpError(400, 'condition filter must be one of new, used');
|
throw new HttpError(400, 'condition filter must be one of new, used');
|
||||||
}
|
}
|
||||||
return value as 'new' | 'used';
|
return value;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import type { ItemStatus } from '@shared/models/item';
|
||||||
import { HttpError } from '../errors/http-error.js';
|
import { HttpError } from '../errors/http-error.js';
|
||||||
import { parseUuid } from './common.js';
|
import { parseUuid } from './common.js';
|
||||||
|
|
||||||
const itemStatuses = new Set(['active', 'rejected']);
|
const itemStatuses = new Set<ItemStatus>(['active', 'rejected']);
|
||||||
|
|
||||||
|
const isItemStatus = (value: unknown): value is ItemStatus => {
|
||||||
|
return typeof value === 'string' && itemStatuses.has(value as ItemStatus);
|
||||||
|
};
|
||||||
|
|
||||||
const ensureAttributesObject = (value: unknown): Record<string, unknown> => {
|
const ensureAttributesObject = (value: unknown): Record<string, unknown> => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
@@ -18,7 +23,7 @@ export interface ItemCreateInput {
|
|||||||
model: string;
|
model: string;
|
||||||
sourceUrlId: string | null;
|
sourceUrlId: string | null;
|
||||||
sourceUrl: string | null;
|
sourceUrl: string | null;
|
||||||
status: 'active' | 'rejected';
|
status: ItemStatus;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
attributes: Record<string, unknown>;
|
attributes: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
@@ -55,7 +60,7 @@ export const parseItemCreatePayload = (payload: unknown): ItemCreateInput => {
|
|||||||
throw new HttpError(400, 'model must be a non-empty string up to 150 characters');
|
throw new HttpError(400, 'model must be a non-empty string up to 150 characters');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof status !== 'string' || !itemStatuses.has(status)) {
|
if (!isItemStatus(status)) {
|
||||||
throw new HttpError(400, 'status must be one of active, rejected');
|
throw new HttpError(400, 'status must be one of active, rejected');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +85,7 @@ export const parseItemCreatePayload = (payload: unknown): ItemCreateInput => {
|
|||||||
manufacturer:
|
manufacturer:
|
||||||
manufacturerValue === null ? null : (manufacturerValue as string).trim(),
|
manufacturerValue === null ? null : (manufacturerValue as string).trim(),
|
||||||
model: model.trim(),
|
model: model.trim(),
|
||||||
status: status as 'active' | 'rejected',
|
status: status as ItemStatus,
|
||||||
note: note === null ? null : (note as string),
|
note: note === null ? null : (note as string),
|
||||||
attributes: ensureAttributesObject(attributes),
|
attributes: ensureAttributesObject(attributes),
|
||||||
sourceUrlId: normalizedSourceUrlId,
|
sourceUrlId: normalizedSourceUrlId,
|
||||||
@@ -127,10 +132,10 @@ export const parseItemUpdatePayload = (payload: unknown): ItemUpdateInput => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status !== undefined) {
|
if (status !== undefined) {
|
||||||
if (typeof status !== 'string' || !itemStatuses.has(status)) {
|
if (!isItemStatus(status)) {
|
||||||
throw new HttpError(400, 'status must be one of active, rejected');
|
throw new HttpError(400, 'status must be one of active, rejected');
|
||||||
}
|
}
|
||||||
result.status = status as 'active' | 'rejected';
|
result.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note !== undefined) {
|
if (note !== undefined) {
|
||||||
@@ -174,14 +179,14 @@ export const parseItemUpdatePayload = (payload: unknown): ItemUpdateInput => {
|
|||||||
|
|
||||||
export const parseItemStatusFilter = (
|
export const parseItemStatusFilter = (
|
||||||
value: unknown
|
value: unknown
|
||||||
): 'active' | 'rejected' | undefined => {
|
): ItemStatus | undefined => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (typeof value !== 'string' || !itemStatuses.has(value)) {
|
if (!isItemStatus(value)) {
|
||||||
throw new HttpError(400, 'status filter must be one of active, rejected');
|
throw new HttpError(400, 'status filter must be one of active, rejected');
|
||||||
}
|
}
|
||||||
return value as 'active' | 'rejected';
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ItemImportPayload {
|
export interface ItemImportPayload {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import type { ProjectStatus } from '@shared/models/project';
|
||||||
import { HttpError } from '../errors/http-error.js';
|
import { HttpError } from '../errors/http-error.js';
|
||||||
|
|
||||||
const projectStatuses = new Set(['active', 'archived']);
|
const projectStatuses = new Set<ProjectStatus>(['active', 'archived']);
|
||||||
|
|
||||||
|
const isProjectStatus = (value: unknown): value is ProjectStatus => {
|
||||||
|
return typeof value === 'string' && projectStatuses.has(value as ProjectStatus);
|
||||||
|
};
|
||||||
|
|
||||||
const isStringArray = (value: unknown): value is string[] => {
|
const isStringArray = (value: unknown): value is string[] => {
|
||||||
return (
|
return (
|
||||||
@@ -12,7 +17,7 @@ const isStringArray = (value: unknown): value is string[] => {
|
|||||||
export interface ProjectCreateInput {
|
export interface ProjectCreateInput {
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
status: 'active' | 'archived';
|
status: ProjectStatus;
|
||||||
projectAttributes: string[];
|
projectAttributes: string[];
|
||||||
projectPriorityRules: unknown;
|
projectPriorityRules: unknown;
|
||||||
}
|
}
|
||||||
@@ -40,7 +45,7 @@ export const parseProjectCreatePayload = (payload: unknown): ProjectCreateInput
|
|||||||
throw new HttpError(400, 'description must be a string or null');
|
throw new HttpError(400, 'description must be a string or null');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof status !== 'string' || !projectStatuses.has(status)) {
|
if (!isProjectStatus(status)) {
|
||||||
throw new HttpError(400, 'status must be one of active, archived');
|
throw new HttpError(400, 'status must be one of active, archived');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +57,7 @@ export const parseProjectCreatePayload = (payload: unknown): ProjectCreateInput
|
|||||||
return {
|
return {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
description: description === null ? null : description,
|
description: description === null ? null : description,
|
||||||
status: status as 'active' | 'archived',
|
status: status as ProjectStatus,
|
||||||
projectAttributes,
|
projectAttributes,
|
||||||
projectPriorityRules: priorityRules ?? []
|
projectPriorityRules: priorityRules ?? []
|
||||||
};
|
};
|
||||||
@@ -84,10 +89,10 @@ export const parseProjectUpdatePayload = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status !== undefined) {
|
if (status !== undefined) {
|
||||||
if (typeof status !== 'string' || !projectStatuses.has(status)) {
|
if (!isProjectStatus(status)) {
|
||||||
throw new HttpError(400, 'status must be one of active, archived');
|
throw new HttpError(400, 'status must be one of active, archived');
|
||||||
}
|
}
|
||||||
result.status = status as 'active' | 'archived';
|
result.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attributes !== undefined) {
|
if (attributes !== undefined) {
|
||||||
@@ -108,12 +113,12 @@ export const parseProjectUpdatePayload = (
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseProjectStatusFilter = (value: unknown): 'active' | 'archived' | undefined => {
|
export const parseProjectStatusFilter = (value: unknown): ProjectStatus | undefined => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (typeof value !== 'string' || !projectStatuses.has(value)) {
|
if (!isProjectStatus(value)) {
|
||||||
throw new HttpError(400, 'status filter must be one of active, archived');
|
throw new HttpError(400, 'status filter must be one of active, archived');
|
||||||
}
|
}
|
||||||
return value as 'active' | 'archived';
|
return value;
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-1
@@ -2,7 +2,17 @@
|
|||||||
"extends": "../tsconfig.base.json",
|
"extends": "../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src"
|
"rootDir": "src",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"rootDirs": [
|
||||||
|
"src",
|
||||||
|
"../shared"
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@shared/*": [
|
||||||
|
"../shared/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
|
|||||||
+7
-1
@@ -7,6 +7,12 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@shared/*": [
|
||||||
|
"shared/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user