add shared

This commit is contained in:
Jurgis Sakalauskas
2025-10-14 09:14:01 +03:00
parent 3ac7e9183f
commit b20ab30ece
19 changed files with 134 additions and 211 deletions
+8 -29
View File
@@ -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
View File
@@ -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;
+3 -23
View File
@@ -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[];
+1 -1
View File
@@ -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;
+4 -1
View File
@@ -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"
+8
View File
@@ -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')
} }
} }
}; };
+10
View File
@@ -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",
+9 -23
View File
@@ -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;
+6 -34
View File
@@ -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>;
} }
+5 -15
View File
@@ -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;
} }
+27 -18
View File
@@ -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;
}; };
+14 -9
View File
@@ -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 {
+14 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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"
]
}
} }
} }