Update TypeScript and ESLint configurations for improved type safety and compatibility
- Modified ESLint configuration to support file patterns for TypeScript files. - Updated TypeScript configurations across multiple applications to use ES2024 and enable composite builds. - Refactored type inference in domain modules to utilize Zod's infer type for better type safety. - Enhanced utility functions to handle various data types more robustly, improving overall code quality.
This commit is contained in:
parent
776cf3eeeb
commit
ece89de49a
@ -5,6 +5,8 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"strictPropertyInitialization": false,
|
"strictPropertyInitialization": false,
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": ".typecheck/tsconfig.tsbuildinfo",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"],
|
"@/*": ["src/*"],
|
||||||
|
|||||||
@ -2,12 +2,14 @@
|
|||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"plugins": [{ "name": "next" }],
|
"plugins": [{ "name": "next" }],
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": ".typecheck/tsconfig.tsbuildinfo",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export default [
|
|||||||
// TypeScript recommended rules (fast, no type info)
|
// TypeScript recommended rules (fast, no type info)
|
||||||
...tseslint.configs.recommended.map((config) => ({
|
...tseslint.configs.recommended.map((config) => ({
|
||||||
...config,
|
...config,
|
||||||
files: ["**/*.ts", "**/*.tsx"],
|
files: ["**/*.{ts,tsx}"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
...(config.languageOptions || {}),
|
...(config.languageOptions || {}),
|
||||||
// Keep config simple: allow both environments; app-specific blocks can tighten later
|
// Keep config simple: allow both environments; app-specific blocks can tighten later
|
||||||
@ -64,12 +64,6 @@ export default [
|
|||||||
// Backend & domain packages
|
// Backend & domain packages
|
||||||
{
|
{
|
||||||
files: ["apps/bff/**/*.ts", "packages/domain/**/*.ts"],
|
files: ["apps/bff/**/*.ts", "packages/domain/**/*.ts"],
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
projectService: true,
|
|
||||||
tsconfigRootDir: process.cwd(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/consistent-type-imports": "error",
|
"@typescript-eslint/consistent-type-imports": "error",
|
||||||
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
|
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
|
||||||
@ -178,7 +172,12 @@ export default [
|
|||||||
|
|
||||||
// Node globals for config files
|
// Node globals for config files
|
||||||
{
|
{
|
||||||
files: ["*.config.mjs", "apps/portal/next.config.mjs"],
|
files: [
|
||||||
|
"*.config.*",
|
||||||
|
"apps/portal/next.config.mjs",
|
||||||
|
"config/**/*.{js,cjs,mjs}",
|
||||||
|
"scripts/**/*.{js,cjs,mjs}",
|
||||||
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: { ...globals.node },
|
globals: { ...globals.node },
|
||||||
},
|
},
|
||||||
|
|||||||
@ -68,8 +68,8 @@ export const VALID_INVOICE_LIST_STATUSES = [
|
|||||||
/**
|
/**
|
||||||
* Check if a status string is valid for invoices
|
* Check if a status string is valid for invoices
|
||||||
*/
|
*/
|
||||||
export function isValidInvoiceStatus(status: string): boolean {
|
export function isValidInvoiceStatus(status: string): status is ValidInvoiceStatus {
|
||||||
return VALID_INVOICE_STATUSES.includes(status as any);
|
return (VALID_INVOICE_STATUSES as readonly string[]).includes(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod";
|
import type { infer as ZodInfer } from "zod";
|
||||||
import {
|
import type {
|
||||||
activityTypeSchema,
|
activityTypeSchema,
|
||||||
activitySchema,
|
activitySchema,
|
||||||
dashboardStatsSchema,
|
dashboardStatsSchema,
|
||||||
@ -11,12 +11,12 @@ import {
|
|||||||
dashboardSummaryResponseSchema,
|
dashboardSummaryResponseSchema,
|
||||||
} from "./schema.js";
|
} from "./schema.js";
|
||||||
|
|
||||||
export type ActivityType = z.infer<typeof activityTypeSchema>;
|
export type ActivityType = ZodInfer<typeof activityTypeSchema>;
|
||||||
export type Activity = z.infer<typeof activitySchema>;
|
export type Activity = ZodInfer<typeof activitySchema>;
|
||||||
export type DashboardStats = z.infer<typeof dashboardStatsSchema>;
|
export type DashboardStats = ZodInfer<typeof dashboardStatsSchema>;
|
||||||
export type NextInvoice = z.infer<typeof nextInvoiceSchema>;
|
export type NextInvoice = ZodInfer<typeof nextInvoiceSchema>;
|
||||||
export type DashboardSummary = z.infer<typeof dashboardSummarySchema>;
|
export type DashboardSummary = ZodInfer<typeof dashboardSummarySchema>;
|
||||||
export type DashboardError = z.infer<typeof dashboardErrorSchema>;
|
export type DashboardError = ZodInfer<typeof dashboardErrorSchema>;
|
||||||
export type ActivityFilter = z.infer<typeof activityFilterSchema>;
|
export type ActivityFilter = ZodInfer<typeof activityFilterSchema>;
|
||||||
export type ActivityFilterConfig = z.infer<typeof activityFilterConfigSchema>;
|
export type ActivityFilterConfig = ZodInfer<typeof activityFilterConfigSchema>;
|
||||||
export type DashboardSummaryResponse = z.infer<typeof dashboardSummaryResponseSchema>;
|
export type DashboardSummaryResponse = ZodInfer<typeof dashboardSummaryResponseSchema>;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
* These rules represent domain logic and should be reusable across frontend/backend.
|
* These rules represent domain logic and should be reusable across frontend/backend.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from "zod";
|
import type { infer as ZodInfer } from "zod";
|
||||||
import { orderBusinessValidationSchema } from "./schema.js";
|
import { orderBusinessValidationSchema } from "./schema.js";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -18,7 +18,7 @@ import { orderBusinessValidationSchema } from "./schema.js";
|
|||||||
*/
|
*/
|
||||||
export function hasSimServicePlan(skus: string[]): boolean {
|
export function hasSimServicePlan(skus: string[]): boolean {
|
||||||
return skus.some(
|
return skus.some(
|
||||||
(sku) =>
|
sku =>
|
||||||
sku.toUpperCase().includes("SIM") &&
|
sku.toUpperCase().includes("SIM") &&
|
||||||
!sku.toUpperCase().includes("ACTIVATION") &&
|
!sku.toUpperCase().includes("ACTIVATION") &&
|
||||||
!sku.toUpperCase().includes("ADDON")
|
!sku.toUpperCase().includes("ADDON")
|
||||||
@ -38,9 +38,7 @@ export function hasSimActivationFee(_skus: string[]): boolean {
|
|||||||
*/
|
*/
|
||||||
export function hasVpnActivationFee(skus: string[]): boolean {
|
export function hasVpnActivationFee(skus: string[]): boolean {
|
||||||
return skus.some(
|
return skus.some(
|
||||||
(sku) =>
|
sku => sku.toUpperCase().includes("VPN") && sku.toUpperCase().includes("ACTIVATION")
|
||||||
sku.toUpperCase().includes("VPN") &&
|
|
||||||
sku.toUpperCase().includes("ACTIVATION")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +48,7 @@ export function hasVpnActivationFee(skus: string[]): boolean {
|
|||||||
*/
|
*/
|
||||||
export function hasInternetServicePlan(skus: string[]): boolean {
|
export function hasInternetServicePlan(skus: string[]): boolean {
|
||||||
return skus.some(
|
return skus.some(
|
||||||
(sku) =>
|
sku =>
|
||||||
sku.toUpperCase().includes("INTERNET") &&
|
sku.toUpperCase().includes("INTERNET") &&
|
||||||
!sku.toUpperCase().includes("INSTALL") &&
|
!sku.toUpperCase().includes("INSTALL") &&
|
||||||
!sku.toUpperCase().includes("ADDON")
|
!sku.toUpperCase().includes("ADDON")
|
||||||
@ -62,7 +60,7 @@ export function hasInternetServicePlan(skus: string[]): boolean {
|
|||||||
* (filters out installation, addons, activation fees)
|
* (filters out installation, addons, activation fees)
|
||||||
*/
|
*/
|
||||||
export function getMainServiceSkus(skus: string[]): string[] {
|
export function getMainServiceSkus(skus: string[]): string[] {
|
||||||
return skus.filter((sku) => {
|
return skus.filter(sku => {
|
||||||
const upperSku = sku.toUpperCase();
|
const upperSku = sku.toUpperCase();
|
||||||
return (
|
return (
|
||||||
!upperSku.includes("INSTALL") &&
|
!upperSku.includes("INSTALL") &&
|
||||||
@ -90,29 +88,20 @@ export function getMainServiceSkus(skus: string[]): string[] {
|
|||||||
* - Internet orders have service plan
|
* - Internet orders have service plan
|
||||||
*/
|
*/
|
||||||
export const orderWithSkuValidationSchema = orderBusinessValidationSchema
|
export const orderWithSkuValidationSchema = orderBusinessValidationSchema
|
||||||
.refine(
|
.refine(data => data.orderType !== "SIM" || hasSimServicePlan(data.skus), {
|
||||||
(data) => data.orderType !== "SIM" || hasSimServicePlan(data.skus),
|
message: "SIM orders must include a SIM service plan",
|
||||||
{
|
path: ["skus"],
|
||||||
message: "SIM orders must include a SIM service plan",
|
})
|
||||||
path: ["skus"],
|
.refine(data => data.orderType !== "VPN" || hasVpnActivationFee(data.skus), {
|
||||||
}
|
message: "VPN orders require an activation fee",
|
||||||
)
|
path: ["skus"],
|
||||||
.refine(
|
})
|
||||||
(data) => data.orderType !== "VPN" || hasVpnActivationFee(data.skus),
|
.refine(data => data.orderType !== "Internet" || hasInternetServicePlan(data.skus), {
|
||||||
{
|
message: "Internet orders require a service plan",
|
||||||
message: "VPN orders require an activation fee",
|
path: ["skus"],
|
||||||
path: ["skus"],
|
});
|
||||||
}
|
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(data) => data.orderType !== "Internet" || hasInternetServicePlan(data.skus),
|
|
||||||
{
|
|
||||||
message: "Internet orders require a service plan",
|
|
||||||
path: ["skus"],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type OrderWithSkuValidation = z.infer<typeof orderWithSkuValidationSchema>;
|
export type OrderWithSkuValidation = ZodInfer<typeof orderWithSkuValidationSchema>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Validation Error Messages
|
// Validation Error Messages
|
||||||
@ -143,4 +132,3 @@ export function getOrderTypeValidationError(orderType: string, skus: string[]):
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,10 @@ export function normalizeCycle<T extends string>(
|
|||||||
defaultCycle: T
|
defaultCycle: T
|
||||||
): T {
|
): T {
|
||||||
if (!cycle) return defaultCycle;
|
if (!cycle) return defaultCycle;
|
||||||
const normalized = cycle.trim().toLowerCase().replace(/[_\s-]+/g, " ");
|
const normalized = cycle
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[_\s-]+/g, " ");
|
||||||
return cycleMap[normalized] ?? defaultCycle;
|
return cycleMap[normalized] ?? defaultCycle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,17 +95,22 @@ export function getCustomFieldsMap(customFields: unknown): Record<string, string
|
|||||||
|
|
||||||
const map: Record<string, string> = {};
|
const map: Record<string, string> = {};
|
||||||
for (const entry of normalizeCustomFieldEntries(customFields)) {
|
for (const entry of normalizeCustomFieldEntries(customFields)) {
|
||||||
|
const idRaw = "id" in entry ? entry.id : undefined;
|
||||||
const id =
|
const id =
|
||||||
"id" in entry && entry.id !== undefined && entry.id !== null
|
typeof idRaw === "string"
|
||||||
? String(entry.id).trim()
|
? idRaw.trim()
|
||||||
: undefined;
|
: typeof idRaw === "number"
|
||||||
const name =
|
? String(idRaw)
|
||||||
"name" in entry && typeof entry.name === "string"
|
: undefined;
|
||||||
? entry.name.trim()
|
const name = "name" in entry && typeof entry.name === "string" ? entry.name.trim() : undefined;
|
||||||
: undefined;
|
|
||||||
const rawValue = "value" in entry ? entry.value : undefined;
|
const rawValue = "value" in entry ? entry.value : undefined;
|
||||||
if (rawValue === undefined || rawValue === null) continue;
|
if (rawValue === undefined || rawValue === null) continue;
|
||||||
const value = typeof rawValue === "string" ? rawValue : String(rawValue);
|
const value =
|
||||||
|
typeof rawValue === "string"
|
||||||
|
? rawValue
|
||||||
|
: typeof rawValue === "number" || typeof rawValue === "boolean"
|
||||||
|
? String(rawValue)
|
||||||
|
: undefined;
|
||||||
if (!value) continue;
|
if (!value) continue;
|
||||||
|
|
||||||
if (id) map[id] = value;
|
if (id) map[id] = value;
|
||||||
|
|||||||
@ -90,11 +90,12 @@ export function transformFreebitAccountDetails(raw: unknown): SimDetails {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sanitizedAccount = asString(account.account);
|
const sanitizedAccount = asString(account.account);
|
||||||
const simSizeValue = account.simSize ?? (account as any).size;
|
const legacySize = "size" in account ? (account as { size?: unknown }).size : undefined;
|
||||||
|
const simSizeValue = account.simSize ?? legacySize;
|
||||||
const eidValue = account.eid;
|
const eidValue = account.eid;
|
||||||
const simType = deriveSimType(
|
const simType = deriveSimType(
|
||||||
typeof simSizeValue === 'number' ? String(simSizeValue) : simSizeValue,
|
typeof simSizeValue === "number" ? String(simSizeValue) : simSizeValue,
|
||||||
typeof eidValue === 'number' ? String(eidValue) : eidValue
|
typeof eidValue === "number" ? String(eidValue) : eidValue
|
||||||
);
|
);
|
||||||
const voiceMailEnabled = parseBooleanFlag(account.voicemail ?? account.voiceMail);
|
const voiceMailEnabled = parseBooleanFlag(account.voicemail ?? account.voiceMail);
|
||||||
const callWaitingEnabled = parseBooleanFlag(account.callwaiting ?? account.callWaiting);
|
const callWaitingEnabled = parseBooleanFlag(account.callwaiting ?? account.callWaiting);
|
||||||
@ -202,7 +203,9 @@ export function transformFreebitEsimAddAccountResponse(raw: unknown) {
|
|||||||
return freebitEsimAddAccountRawSchema.parse(raw);
|
return freebitEsimAddAccountRawSchema.parse(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FreebitEsimAddAccountResponse = ReturnType<typeof transformFreebitEsimAddAccountResponse>;
|
export type FreebitEsimAddAccountResponse = ReturnType<
|
||||||
|
typeof transformFreebitEsimAddAccountResponse
|
||||||
|
>;
|
||||||
|
|
||||||
export function transformFreebitEsimActivationResponse(raw: unknown) {
|
export function transformFreebitEsimActivationResponse(raw: unknown) {
|
||||||
return freebitEsimAddAccountRawSchema.parse(raw);
|
return freebitEsimAddAccountRawSchema.parse(raw);
|
||||||
|
|||||||
@ -33,9 +33,7 @@ export function isSimSubscription(subscription: Subscription): boolean {
|
|||||||
* @param subscription - The subscription to extract the account from
|
* @param subscription - The subscription to extract the account from
|
||||||
* @returns The SIM account identifier (phone number), or null if not found
|
* @returns The SIM account identifier (phone number), or null if not found
|
||||||
*/
|
*/
|
||||||
export function extractSimAccountFromSubscription(
|
export function extractSimAccountFromSubscription(subscription: Subscription): string | null {
|
||||||
subscription: Subscription
|
|
||||||
): string | null {
|
|
||||||
// 1. Try domain field first (most common location)
|
// 1. Try domain field first (most common location)
|
||||||
if (subscription.domain && subscription.domain.trim()) {
|
if (subscription.domain && subscription.domain.trim()) {
|
||||||
return subscription.domain.trim();
|
return subscription.domain.trim();
|
||||||
@ -67,9 +65,7 @@ export function extractSimAccountFromSubscription(
|
|||||||
* @param customFields - The custom fields record to search
|
* @param customFields - The custom fields record to search
|
||||||
* @returns The SIM account if found, null otherwise
|
* @returns The SIM account if found, null otherwise
|
||||||
*/
|
*/
|
||||||
function extractFromCustomFields(
|
function extractFromCustomFields(customFields: Record<string, unknown>): string | null {
|
||||||
customFields: Record<string, unknown>
|
|
||||||
): string | null {
|
|
||||||
// Known field names for SIM phone numbers across different WHMCS configurations
|
// Known field names for SIM phone numbers across different WHMCS configurations
|
||||||
const phoneFields = [
|
const phoneFields = [
|
||||||
// Standard field names
|
// Standard field names
|
||||||
@ -109,7 +105,15 @@ function extractFromCustomFields(
|
|||||||
for (const fieldName of phoneFields) {
|
for (const fieldName of phoneFields) {
|
||||||
const value = customFields[fieldName];
|
const value = customFields[fieldName];
|
||||||
if (value !== undefined && value !== null && value !== "") {
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
return String(value);
|
if (typeof value === "string") {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed) return trimmed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "number" || typeof value === "boolean") {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,4 +132,3 @@ function extractFromCustomFields(
|
|||||||
export function cleanSimAccount(account: string): string {
|
export function cleanSimAccount(account: string): string {
|
||||||
return account.replace(/[-\s()]/g, "");
|
return account.replace(/[-\s()]/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,17 @@
|
|||||||
* Transforms raw WHMCS product/service data into normalized subscription types.
|
* Transforms raw WHMCS product/service data into normalized subscription types.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Subscription, SubscriptionStatus, SubscriptionCycle, SubscriptionList } from "../../contract.js";
|
import type {
|
||||||
import { subscriptionSchema, subscriptionListSchema, subscriptionStatusSchema } from "../../schema.js";
|
Subscription,
|
||||||
|
SubscriptionStatus,
|
||||||
|
SubscriptionCycle,
|
||||||
|
SubscriptionList,
|
||||||
|
} from "../../contract.js";
|
||||||
|
import {
|
||||||
|
subscriptionSchema,
|
||||||
|
subscriptionListSchema,
|
||||||
|
subscriptionStatusSchema,
|
||||||
|
} from "../../schema.js";
|
||||||
import {
|
import {
|
||||||
type WhmcsProductRaw,
|
type WhmcsProductRaw,
|
||||||
whmcsProductRawSchema,
|
whmcsProductRawSchema,
|
||||||
@ -25,7 +34,7 @@ export interface TransformSubscriptionOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TransformSubscriptionListResponseOptions extends TransformSubscriptionOptions {
|
export interface TransformSubscriptionListResponseOptions extends TransformSubscriptionOptions {
|
||||||
status?: SubscriptionStatus | string;
|
status?: string;
|
||||||
onItemError?: (error: unknown, product: WhmcsProductRaw) => void;
|
onItemError?: (error: unknown, product: WhmcsProductRaw) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,10 +117,10 @@ export function transformWhmcsSubscription(
|
|||||||
// Determine amount
|
// Determine amount
|
||||||
const amount = parseAmount(
|
const amount = parseAmount(
|
||||||
product.amount ||
|
product.amount ||
|
||||||
product.recurringamount ||
|
product.recurringamount ||
|
||||||
product.pricing?.amount ||
|
product.pricing?.amount ||
|
||||||
product.firstpaymentamount ||
|
product.firstpaymentamount ||
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
// Transform to domain model
|
// Transform to domain model
|
||||||
@ -203,7 +212,7 @@ export function transformWhmcsSubscriptionListResponse(
|
|||||||
|
|
||||||
export function filterSubscriptionsByStatus(
|
export function filterSubscriptionsByStatus(
|
||||||
list: SubscriptionList,
|
list: SubscriptionList,
|
||||||
status: SubscriptionStatus | string
|
status: string
|
||||||
): SubscriptionList {
|
): SubscriptionList {
|
||||||
const normalizedStatus = subscriptionStatusSchema.parse(status);
|
const normalizedStatus = subscriptionStatusSchema.parse(status);
|
||||||
const filtered = list.subscriptions.filter(sub => sub.status === normalizedStatus);
|
const filtered = list.subscriptions.filter(sub => sub.status === normalizedStatus);
|
||||||
|
|||||||
@ -17,9 +17,7 @@ export type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
|
|||||||
/**
|
/**
|
||||||
* Deep partial (makes all nested properties optional)
|
* Deep partial (makes all nested properties optional)
|
||||||
*/
|
*/
|
||||||
export type DeepPartial<T> = T extends object
|
export type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
|
||||||
? { [P in keyof T]?: DeepPartial<T[P]> }
|
|
||||||
: T;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract keys of a certain type
|
* Extract keys of a certain type
|
||||||
@ -51,22 +49,25 @@ export type PickByValue<T, V> = Pick<
|
|||||||
/**
|
/**
|
||||||
* Safely access nested property
|
* Safely access nested property
|
||||||
*/
|
*/
|
||||||
export function getNestedProperty<T>(
|
export function getNestedProperty<T>(obj: unknown, path: string, defaultValue?: T): T | undefined {
|
||||||
obj: unknown,
|
|
||||||
path: string,
|
|
||||||
defaultValue?: T
|
|
||||||
): T | undefined {
|
|
||||||
const keys = path.split(".");
|
const keys = path.split(".");
|
||||||
let current: any = obj;
|
let current: unknown = obj;
|
||||||
|
|
||||||
|
const isIndexableObject = (value: unknown): value is Record<string, unknown> =>
|
||||||
|
typeof value === "object" && value !== null;
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (current === null || current === undefined || typeof current !== "object") {
|
if (!isIndexableObject(current)) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
current = current[key];
|
current = current[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
return current ?? defaultValue;
|
if (current === undefined || current === null) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -113,36 +114,27 @@ export function createErrorState<T, E = Error>(error: E): AsyncState<T, E> {
|
|||||||
/**
|
/**
|
||||||
* Type guard: check if state is idle
|
* Type guard: check if state is idle
|
||||||
*/
|
*/
|
||||||
export function isIdle<T, E>(
|
export function isIdle<T, E>(state: AsyncState<T, E>): state is { status: "idle" } {
|
||||||
state: AsyncState<T, E>
|
|
||||||
): state is { status: "idle" } {
|
|
||||||
return state.status === "idle";
|
return state.status === "idle";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type guard: check if state is loading
|
* Type guard: check if state is loading
|
||||||
*/
|
*/
|
||||||
export function isLoading<T, E>(
|
export function isLoading<T, E>(state: AsyncState<T, E>): state is { status: "loading" } {
|
||||||
state: AsyncState<T, E>
|
|
||||||
): state is { status: "loading" } {
|
|
||||||
return state.status === "loading";
|
return state.status === "loading";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type guard: check if state is success
|
* Type guard: check if state is success
|
||||||
*/
|
*/
|
||||||
export function isSuccess<T, E>(
|
export function isSuccess<T, E>(state: AsyncState<T, E>): state is { status: "success"; data: T } {
|
||||||
state: AsyncState<T, E>
|
|
||||||
): state is { status: "success"; data: T } {
|
|
||||||
return state.status === "success";
|
return state.status === "success";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type guard: check if state is error
|
* Type guard: check if state is error
|
||||||
*/
|
*/
|
||||||
export function isError<T, E>(
|
export function isError<T, E>(state: AsyncState<T, E>): state is { status: "error"; error: E } {
|
||||||
state: AsyncState<T, E>
|
|
||||||
): state is { status: "error"; error: E } {
|
|
||||||
return state.status === "error";
|
return state.status === "error";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,12 +41,7 @@ export function sanitizePagination(options: {
|
|||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
} {
|
} {
|
||||||
const {
|
const { page = 1, limit = options.defaultLimit ?? 10, minLimit = 1, maxLimit = 100 } = options;
|
||||||
page = 1,
|
|
||||||
limit = options.defaultLimit ?? 10,
|
|
||||||
minLimit = 1,
|
|
||||||
maxLimit = 100,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
page: Math.max(1, Math.floor(page)),
|
page: Math.max(1, Math.floor(page)),
|
||||||
@ -79,7 +74,7 @@ export function isValidEnumValue<T extends Record<string, string | number>>(
|
|||||||
value: unknown,
|
value: unknown,
|
||||||
enumObj: T
|
enumObj: T
|
||||||
): value is T[keyof T] {
|
): value is T[keyof T] {
|
||||||
return Object.values(enumObj).includes(value as any);
|
return Object.values(enumObj).some(enumValue => enumValue === value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -107,11 +102,7 @@ export function hasUniqueItems<T>(items: T[]): boolean {
|
|||||||
/**
|
/**
|
||||||
* Check if number is within range (inclusive)
|
* Check if number is within range (inclusive)
|
||||||
*/
|
*/
|
||||||
export function isInRange(
|
export function isInRange(value: number, min: number, max: number): boolean {
|
||||||
value: number,
|
|
||||||
min: number,
|
|
||||||
max: number
|
|
||||||
): boolean {
|
|
||||||
return value >= min && value <= max;
|
return value >= min && value <= max;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,13 +163,7 @@ export function createPaginationSchema(options?: {
|
|||||||
|
|
||||||
return z.object({
|
return z.object({
|
||||||
page: z.coerce.number().int().positive().optional().default(1),
|
page: z.coerce.number().int().positive().optional().default(1),
|
||||||
limit: z.coerce
|
limit: z.coerce.number().int().min(minLimit).max(maxLimit).optional().default(defaultLimit),
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(minLimit)
|
|
||||||
.max(maxLimit)
|
|
||||||
.optional()
|
|
||||||
.default(defaultLimit),
|
|
||||||
offset: z.coerce.number().int().nonnegative().optional(),
|
offset: z.coerce.number().int().nonnegative().optional(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -206,11 +191,6 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
* Type guard for checking if error is a Zod error
|
* Type guard for checking if error is a Zod error
|
||||||
*/
|
*/
|
||||||
export function isZodError(error: unknown): error is z.ZodError {
|
export function isZodError(error: unknown): error is z.ZodError {
|
||||||
return (
|
if (!isRecord(error)) return false;
|
||||||
typeof error === "object" &&
|
return Array.isArray(error.issues);
|
||||||
error !== null &&
|
|
||||||
"issues" in error &&
|
|
||||||
Array.isArray((error as any).issues)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2024",
|
"target": "ES2024",
|
||||||
"lib": ["ESNext"],
|
"lib": ["ES2024"],
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user