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,3 +1,4 @@
|
||||
import type { ItemPrice, PriceCondition, PriceSourceType } from '@shared/models/item-price';
|
||||
import { pool, query } from './pool.js';
|
||||
|
||||
const priceColumns = `
|
||||
@@ -20,10 +21,10 @@ const priceColumns = `
|
||||
export interface ItemPriceRow {
|
||||
id: string;
|
||||
item_id: string;
|
||||
condition: 'new' | 'used';
|
||||
condition: PriceCondition;
|
||||
amount: string;
|
||||
currency: string;
|
||||
source_type: 'url' | 'manual';
|
||||
source_type: PriceSourceType;
|
||||
source_url_id: string | null;
|
||||
source_note: string | null;
|
||||
note: string | null;
|
||||
@@ -34,22 +35,7 @@ export interface ItemPriceRow {
|
||||
source_url: string | null;
|
||||
}
|
||||
|
||||
export interface ItemPriceRecord {
|
||||
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;
|
||||
}
|
||||
export type ItemPriceRecord = ItemPrice;
|
||||
|
||||
const mapItemPriceRow = (row: ItemPriceRow): ItemPriceRecord => ({
|
||||
id: row.id,
|
||||
@@ -72,7 +58,7 @@ export interface ListItemPricesOptions {
|
||||
itemId: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
condition?: 'new' | 'used';
|
||||
condition?: PriceCondition;
|
||||
}
|
||||
|
||||
export const listItemPrices = async (
|
||||
@@ -109,10 +95,10 @@ export const listItemPrices = async (
|
||||
|
||||
export interface CreateItemPriceParams {
|
||||
itemId: string;
|
||||
condition: 'new' | 'used';
|
||||
condition: PriceCondition;
|
||||
amount: number;
|
||||
currency: string;
|
||||
sourceType: 'url' | 'manual';
|
||||
sourceType: PriceSourceType;
|
||||
sourceUrlId: string | null;
|
||||
sourceNote: string | null;
|
||||
note: string | null;
|
||||
@@ -213,10 +199,10 @@ export const getItemPriceById = async (id: string): Promise<ItemPriceRecord | nu
|
||||
};
|
||||
|
||||
export interface UpdateItemPriceParams {
|
||||
condition?: 'new' | 'used';
|
||||
condition?: PriceCondition;
|
||||
amount?: number;
|
||||
currency?: string;
|
||||
sourceType?: 'url' | 'manual';
|
||||
sourceType?: PriceSourceType;
|
||||
sourceUrlId?: string | null;
|
||||
sourceNote?: string | null;
|
||||
note?: string | null;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Item, ItemPriceSummary, ItemStatus } from '@shared/models/item';
|
||||
import { query } from './pool.js';
|
||||
|
||||
const itemColumns = `
|
||||
@@ -33,7 +34,7 @@ export interface ItemRow {
|
||||
manufacturer: string | null;
|
||||
model: string;
|
||||
source_url_id: string | null;
|
||||
status: 'active' | 'rejected';
|
||||
status: ItemStatus;
|
||||
note: string | null;
|
||||
attributes: Record<string, unknown>;
|
||||
created_at: Date;
|
||||
@@ -54,36 +55,7 @@ export interface ItemRow {
|
||||
price_used_currency_count: number | null;
|
||||
}
|
||||
|
||||
export interface ItemRecord {
|
||||
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;
|
||||
}
|
||||
export type ItemRecord = Item;
|
||||
|
||||
const mapItemRow = (row: ItemRow): ItemRecord => {
|
||||
const priceCount = row.price_count ?? 0;
|
||||
@@ -126,7 +98,7 @@ const mapItemRow = (row: ItemRow): ItemRecord => {
|
||||
|
||||
export interface ListItemsOptions {
|
||||
projectId: string;
|
||||
status?: 'active' | 'rejected';
|
||||
status?: ItemStatus;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
@@ -184,7 +156,7 @@ export interface CreateItemParams {
|
||||
manufacturer: string | null;
|
||||
model: string;
|
||||
sourceUrlId: string | null;
|
||||
status: 'active' | 'rejected';
|
||||
status: ItemStatus;
|
||||
note: string | null;
|
||||
attributes: Record<string, unknown>;
|
||||
}
|
||||
@@ -260,7 +232,7 @@ export interface UpdateItemParams {
|
||||
manufacturer?: string | null;
|
||||
model?: string;
|
||||
sourceUrlId?: string | null;
|
||||
status?: 'active' | 'rejected';
|
||||
status?: ItemStatus;
|
||||
note?: string | null;
|
||||
attributes?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Project, ProjectStatus } from '@shared/models/project';
|
||||
import { query } from './pool.js';
|
||||
|
||||
const projectColumns = `
|
||||
@@ -15,24 +16,13 @@ export interface ProjectRow {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
status: 'active' | 'archived';
|
||||
status: ProjectStatus;
|
||||
project_attributes: string[];
|
||||
project_priority_rules: unknown;
|
||||
created_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 => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
@@ -47,7 +37,7 @@ const mapProjectRow = (row: ProjectRow): Project => ({
|
||||
export interface ListProjectsOptions {
|
||||
limit: number;
|
||||
offset: number;
|
||||
status?: 'active' | 'archived';
|
||||
status?: ProjectStatus;
|
||||
}
|
||||
|
||||
export const listProjects = async (
|
||||
@@ -86,7 +76,7 @@ export const listProjects = async (
|
||||
export interface CreateProjectParams {
|
||||
name: string;
|
||||
description: string | null;
|
||||
status: 'active' | 'archived';
|
||||
status: ProjectStatus;
|
||||
attributes: string[];
|
||||
priorityRules: unknown;
|
||||
}
|
||||
@@ -136,7 +126,7 @@ export const getProjectById = async (id: string): Promise<Project | null> => {
|
||||
export interface UpdateProjectParams {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
status?: 'active' | 'archived';
|
||||
status?: ProjectStatus;
|
||||
attributes?: string[];
|
||||
priorityRules?: unknown;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import type { PriceCondition, PriceSourceType } from '@shared/models/item-price';
|
||||
import { HttpError } from '../errors/http-error.js';
|
||||
import { parseUuid } from './common.js';
|
||||
|
||||
const priceConditions = new Set(['new', 'used']);
|
||||
const priceSources = new Set(['url', 'manual']);
|
||||
const priceConditions = new Set<PriceCondition>(['new', 'used']);
|
||||
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 => {
|
||||
if (typeof value !== 'string' || value.trim().length !== 3) {
|
||||
@@ -29,10 +38,10 @@ const parseObservedAt = (value: unknown): string | undefined => {
|
||||
};
|
||||
|
||||
export interface ItemPriceCreateInput {
|
||||
condition: 'new' | 'used';
|
||||
condition: PriceCondition;
|
||||
amount: number;
|
||||
currency: string;
|
||||
sourceType: 'url' | 'manual';
|
||||
sourceType: PriceSourceType;
|
||||
sourceUrlId: string | null;
|
||||
sourceUrl: string | null;
|
||||
sourceNote: string | null;
|
||||
@@ -81,7 +90,7 @@ const parseIsPrimary = (value: unknown): boolean => {
|
||||
};
|
||||
|
||||
const parseSourceFields = (
|
||||
sourceType: 'url' | 'manual',
|
||||
sourceType: PriceSourceType,
|
||||
sourceUrlIdValue: unknown,
|
||||
sourceUrlValue: unknown
|
||||
): { sourceUrlId: string | null; sourceUrl: string | null } => {
|
||||
@@ -144,25 +153,25 @@ export const parseItemPriceCreatePayload = (
|
||||
isPrimary
|
||||
} = 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');
|
||||
}
|
||||
|
||||
if (typeof sourceType !== 'string' || !priceSources.has(sourceType)) {
|
||||
if (!isPriceSourceType(sourceType)) {
|
||||
throw new HttpError(400, 'sourceType must be one of url, manual');
|
||||
}
|
||||
|
||||
const source = parseSourceFields(
|
||||
sourceType as 'url' | 'manual',
|
||||
sourceType,
|
||||
sourceUrlId,
|
||||
sourceUrl
|
||||
);
|
||||
|
||||
return {
|
||||
condition: condition as 'new' | 'used',
|
||||
condition,
|
||||
amount: parseAmount(amount),
|
||||
currency: normalizeCurrency(currency),
|
||||
sourceType: sourceType as 'url' | 'manual',
|
||||
sourceType,
|
||||
sourceUrlId: source.sourceUrlId,
|
||||
sourceUrl: source.sourceUrl,
|
||||
sourceNote: parseSourceNote(sourceNote),
|
||||
@@ -194,10 +203,10 @@ export const parseItemPriceUpdatePayload = (
|
||||
} = payload as Record<string, unknown>;
|
||||
|
||||
if (condition !== undefined) {
|
||||
if (typeof condition !== 'string' || !priceConditions.has(condition)) {
|
||||
if (!isPriceCondition(condition)) {
|
||||
throw new HttpError(400, 'condition must be one of new, used');
|
||||
}
|
||||
result.condition = condition as 'new' | 'used';
|
||||
result.condition = condition;
|
||||
}
|
||||
|
||||
if (amount !== undefined) {
|
||||
@@ -209,14 +218,14 @@ export const parseItemPriceUpdatePayload = (
|
||||
}
|
||||
|
||||
if (sourceType !== undefined) {
|
||||
if (typeof sourceType !== 'string' || !priceSources.has(sourceType)) {
|
||||
if (!isPriceSourceType(sourceType)) {
|
||||
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) {
|
||||
const type = result.sourceType ?? (sourceType as 'url' | 'manual' | undefined);
|
||||
const type = result.sourceType ?? (isPriceSourceType(sourceType) ? sourceType : undefined);
|
||||
if (!type) {
|
||||
throw new HttpError(
|
||||
400,
|
||||
@@ -253,12 +262,12 @@ export const parseItemPriceUpdatePayload = (
|
||||
|
||||
export const parsePriceConditionFilter = (
|
||||
value: unknown
|
||||
): 'new' | 'used' | undefined => {
|
||||
): PriceCondition | undefined => {
|
||||
if (value === 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');
|
||||
}
|
||||
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 { 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> => {
|
||||
if (value === undefined) {
|
||||
@@ -18,7 +23,7 @@ export interface ItemCreateInput {
|
||||
model: string;
|
||||
sourceUrlId: string | null;
|
||||
sourceUrl: string | null;
|
||||
status: 'active' | 'rejected';
|
||||
status: ItemStatus;
|
||||
note: string | null;
|
||||
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');
|
||||
}
|
||||
|
||||
if (typeof status !== 'string' || !itemStatuses.has(status)) {
|
||||
if (!isItemStatus(status)) {
|
||||
throw new HttpError(400, 'status must be one of active, rejected');
|
||||
}
|
||||
|
||||
@@ -80,7 +85,7 @@ export const parseItemCreatePayload = (payload: unknown): ItemCreateInput => {
|
||||
manufacturer:
|
||||
manufacturerValue === null ? null : (manufacturerValue as string).trim(),
|
||||
model: model.trim(),
|
||||
status: status as 'active' | 'rejected',
|
||||
status: status as ItemStatus,
|
||||
note: note === null ? null : (note as string),
|
||||
attributes: ensureAttributesObject(attributes),
|
||||
sourceUrlId: normalizedSourceUrlId,
|
||||
@@ -127,10 +132,10 @@ export const parseItemUpdatePayload = (payload: unknown): ItemUpdateInput => {
|
||||
}
|
||||
|
||||
if (status !== undefined) {
|
||||
if (typeof status !== 'string' || !itemStatuses.has(status)) {
|
||||
if (!isItemStatus(status)) {
|
||||
throw new HttpError(400, 'status must be one of active, rejected');
|
||||
}
|
||||
result.status = status as 'active' | 'rejected';
|
||||
result.status = status;
|
||||
}
|
||||
|
||||
if (note !== undefined) {
|
||||
@@ -174,14 +179,14 @@ export const parseItemUpdatePayload = (payload: unknown): ItemUpdateInput => {
|
||||
|
||||
export const parseItemStatusFilter = (
|
||||
value: unknown
|
||||
): 'active' | 'rejected' | undefined => {
|
||||
): ItemStatus | undefined => {
|
||||
if (value === 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');
|
||||
}
|
||||
return value as 'active' | 'rejected';
|
||||
return value;
|
||||
};
|
||||
|
||||
export interface ItemImportPayload {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { ProjectStatus } from '@shared/models/project';
|
||||
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[] => {
|
||||
return (
|
||||
@@ -12,7 +17,7 @@ const isStringArray = (value: unknown): value is string[] => {
|
||||
export interface ProjectCreateInput {
|
||||
name: string;
|
||||
description: string | null;
|
||||
status: 'active' | 'archived';
|
||||
status: ProjectStatus;
|
||||
projectAttributes: string[];
|
||||
projectPriorityRules: unknown;
|
||||
}
|
||||
@@ -40,7 +45,7 @@ export const parseProjectCreatePayload = (payload: unknown): ProjectCreateInput
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -52,7 +57,7 @@ export const parseProjectCreatePayload = (payload: unknown): ProjectCreateInput
|
||||
return {
|
||||
name: name.trim(),
|
||||
description: description === null ? null : description,
|
||||
status: status as 'active' | 'archived',
|
||||
status: status as ProjectStatus,
|
||||
projectAttributes,
|
||||
projectPriorityRules: priorityRules ?? []
|
||||
};
|
||||
@@ -84,10 +89,10 @@ export const parseProjectUpdatePayload = (
|
||||
}
|
||||
|
||||
if (status !== undefined) {
|
||||
if (typeof status !== 'string' || !projectStatuses.has(status)) {
|
||||
if (!isProjectStatus(status)) {
|
||||
throw new HttpError(400, 'status must be one of active, archived');
|
||||
}
|
||||
result.status = status as 'active' | 'archived';
|
||||
result.status = status;
|
||||
}
|
||||
|
||||
if (attributes !== undefined) {
|
||||
@@ -108,12 +113,12 @@ export const parseProjectUpdatePayload = (
|
||||
return result;
|
||||
};
|
||||
|
||||
export const parseProjectStatusFilter = (value: unknown): 'active' | 'archived' | undefined => {
|
||||
export const parseProjectStatusFilter = (value: unknown): ProjectStatus | undefined => {
|
||||
if (value === 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');
|
||||
}
|
||||
return value as 'active' | 'archived';
|
||||
return value;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user