feat: expand backend admin marketplace and scaling
فشلت بعض الفحوصات
/ deploy (push) Failing after 1m22s

هذا الالتزام موجود في:
2026-05-14 16:17:12 +03:00
الأصل 0e76a4a9fc
التزام 5bd5e19a89
158 ملفات معدلة مع 19563 إضافات و3315 حذوفات

عرض الملف

@@ -0,0 +1,21 @@
import { toNumberArray, toStringArray } from './array-transform.util';
describe('array transform utils', () => {
it('wraps a single string as an array', () => {
expect(toStringArray({ value: '69e8d1f7d1f72ba6416d864b' } as any)).toEqual([
'69e8d1f7d1f72ba6416d864b',
]);
});
it('parses a JSON string array', () => {
expect(toStringArray({ value: '["a","b"]' } as any)).toEqual(['a', 'b']);
});
it('keeps array input as an array', () => {
expect(toStringArray({ value: ['a', 'b'] } as any)).toEqual(['a', 'b']);
});
it('parses numeric arrays from JSON strings', () => {
expect(toNumberArray({ value: '[1,2,3]' } as any)).toEqual([1, 2, 3]);
});
});

عرض الملف

@@ -0,0 +1,69 @@
import { TransformFnParams } from 'class-transformer';
const parseArrayInput = (value: unknown): unknown[] | unknown | undefined => {
if (value === undefined || value === null) {
return undefined;
}
if (Array.isArray(value)) {
return value.flatMap((item) => {
const parsed = parseArrayInput(item);
if (typeof parsed === 'undefined') {
return [];
}
return Array.isArray(parsed) ? parsed : [parsed];
});
}
if (typeof value !== 'string') {
return value;
}
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
try {
return parseArrayInput(JSON.parse(trimmed));
} catch {
return [trimmed];
}
}
if (trimmed.includes(',')) {
return trimmed
.split(',')
.map((part) => part.trim())
.filter(Boolean);
}
return [trimmed];
};
export const toStringArray = ({ value }: TransformFnParams): string[] | unknown | undefined => {
const parsed = parseArrayInput(value);
if (typeof parsed === 'undefined') {
return undefined;
}
if (!Array.isArray(parsed)) {
return parsed;
}
return parsed.map((item) => String(item).trim()).filter(Boolean);
};
export const toNumberArray = ({ value }: TransformFnParams): number[] | unknown | undefined => {
const parsed = parseArrayInput(value);
if (typeof parsed === 'undefined') {
return undefined;
}
if (!Array.isArray(parsed)) {
return parsed;
}
return parsed.map((item) => Number(item));
};

عرض الملف

@@ -1,7 +1,24 @@
import * as bcrypt from 'bcrypt';
import { createHmac, timingSafeEqual } from 'crypto';
export const hashValue = async (value: string, saltRounds: number): Promise<string> =>
bcrypt.hash(value, saltRounds);
export const compareHash = async (value: string, hashedValue: string): Promise<boolean> =>
bcrypt.compare(value, hashedValue);
export const hashHighEntropyValue = (value: string, secret: string): string =>
`sha256:${createHmac('sha256', secret).update(value).digest('hex')}`;
export const compareStoredHighEntropyValue = async (
value: string,
storedValue: string,
secret: string,
): Promise<boolean> => {
if (storedValue.startsWith('sha256:')) {
const nextValue = hashHighEntropyValue(value, secret);
return timingSafeEqual(Buffer.from(nextValue), Buffer.from(storedValue));
}
return compareHash(value, storedValue);
};

عرض الملف

@@ -0,0 +1,37 @@
import { buildPaginatedResponse } from './pagination.util';
describe('pagination util', () => {
it('builds offset pagination metadata', () => {
const result = buildPaginatedResponse(['a', 'b'], {
page: 2,
limit: 2,
total: 5,
offset: 2,
});
expect(result.count).toBe(2);
expect(result.totalPages).toBe(3);
expect(result.pagination.hasNextPage).toBe(true);
expect(result.pagination.hasPreviousPage).toBe(true);
expect(result.pagination.nextPage).toBe(3);
expect(result.pagination.previousPage).toBe(1);
expect(result.pagination.mode).toBe('offset');
});
it('builds cursor pagination metadata', () => {
const result = buildPaginatedResponse(['a'], {
page: 1,
limit: 2,
total: 3,
offset: 0,
currentCursor: 'cursor-a',
nextCursor: 'cursor-b',
mode: 'cursor',
});
expect(result.nextCursor).toBe('cursor-b');
expect(result.pagination.currentCursor).toBe('cursor-a');
expect(result.pagination.nextCursor).toBe('cursor-b');
expect(result.pagination.mode).toBe('cursor');
});
});

عرض الملف

@@ -0,0 +1,70 @@
export type PaginatedResponseOptions = {
page: number;
limit: number;
total: number;
offset: number;
currentCursor?: string | null;
nextCursor?: string | null;
mode?: 'offset' | 'cursor';
};
export type PaginatedResponse<T> = {
items: T[];
count: number;
page: number;
limit: number;
total: number;
totalPages: number;
nextCursor: string | null;
pagination: {
mode: 'offset' | 'cursor';
page: number;
limit: number;
count: number;
total: number;
totalPages: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
nextPage: number | null;
previousPage: number | null;
currentCursor: string | null;
nextCursor: string | null;
};
};
export const buildPaginatedResponse = <T>(
items: T[],
options: PaginatedResponseOptions,
): PaginatedResponse<T> => {
const count = items.length;
const totalPages = Math.ceil(options.total / options.limit) || 1;
const hasNextPage = options.offset + count < options.total;
const hasPreviousPage = options.offset > 0;
const mode =
options.mode ??
(options.currentCursor !== undefined || options.nextCursor !== undefined ? 'cursor' : 'offset');
return {
items,
count,
page: options.page,
limit: options.limit,
total: options.total,
totalPages,
nextCursor: options.nextCursor ?? null,
pagination: {
mode,
page: options.page,
limit: options.limit,
count,
total: options.total,
totalPages,
hasNextPage,
hasPreviousPage,
nextPage: hasNextPage ? options.page + 1 : null,
previousPage: hasPreviousPage ? Math.max(1, options.page - 1) : null,
currentCursor: options.currentCursor ?? null,
nextCursor: options.nextCursor ?? null,
},
};
};

عرض الملف

@@ -0,0 +1,29 @@
import { resolveManagedFileUrl, resolveManagedFileUrls } from './public-url.util';
describe('public url util', () => {
const originalPublicBaseUrl = process.env.PUBLIC_BASE_URL;
const originalStorageBasePath = process.env.STORAGE_BASE_PATH;
beforeEach(() => {
process.env.PUBLIC_BASE_URL = 'http://192.168.1.12:4000';
process.env.STORAGE_BASE_PATH = 'uploads';
});
afterEach(() => {
process.env.PUBLIC_BASE_URL = originalPublicBaseUrl;
process.env.STORAGE_BASE_PATH = originalStorageBasePath;
});
it('resolves a local managed file url', () => {
expect(resolveManagedFileUrl('/uploads/posts/images/file.png')).toBe(
'http://192.168.1.12:4000/uploads/posts/images/file.png',
);
});
it('resolves arrays of local managed file urls', () => {
expect(resolveManagedFileUrls(['/uploads/a.png', '/uploads/b.png'])).toEqual([
'http://192.168.1.12:4000/uploads/a.png',
'http://192.168.1.12:4000/uploads/b.png',
]);
});
});

عرض الملف

@@ -0,0 +1,27 @@
const getUploadsBasePath = (): string =>
`/${(process.env.STORAGE_BASE_PATH ?? 'uploads').replace(/^\/+|\/+$/g, '')}/`;
export const resolveManagedFileUrl = (fileUrl: unknown): unknown => {
if (typeof fileUrl !== 'string' || !fileUrl.trim()) {
return fileUrl;
}
if (!fileUrl.startsWith(getUploadsBasePath())) {
return fileUrl;
}
const baseUrl = (process.env.PUBLIC_BASE_URL ?? '').replace(/\/$/, '');
if (!baseUrl) {
return fileUrl;
}
return `${baseUrl}${fileUrl}`;
};
export const resolveManagedFileUrls = (fileUrls: unknown): unknown => {
if (!Array.isArray(fileUrls)) {
return fileUrls;
}
return fileUrls.map((fileUrl) => resolveManagedFileUrl(fileUrl));
};

عرض الملف

@@ -0,0 +1,16 @@
import { toBoolean } from './query-transform.util';
describe('query transform util', () => {
it('converts "true" and "false" string values correctly', () => {
expect(toBoolean({ value: 'true' })).toBe(true);
expect(toBoolean({ value: 'TRUE' })).toBe(true);
expect(toBoolean({ value: 'false' })).toBe(false);
expect(toBoolean({ value: 'FALSE' })).toBe(false);
});
it('leaves unrelated values unchanged', () => {
expect(toBoolean({ value: '0' })).toBe('0');
expect(toBoolean({ value: 'hello' })).toBe('hello');
expect(toBoolean({ value: 1 })).toBe(1);
});
});

عرض الملف

@@ -0,0 +1,17 @@
export const toBoolean = ({ value }: { value: unknown }): unknown => {
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (normalized === 'true') {
return true;
}
if (normalized === 'false') {
return false;
}
}
if (value === true || value === false) {
return value;
}
return value;
};

عرض الملف

@@ -0,0 +1,13 @@
import { SortOrder } from '../enums/sort-order.enum';
import { resolveMongoSortDirection } from './sort.util';
describe('sort util', () => {
it('returns ascending for asc', () => {
expect(resolveMongoSortDirection(SortOrder.ASC)).toBe(1);
});
it('returns descending by default', () => {
expect(resolveMongoSortDirection(undefined)).toBe(-1);
expect(resolveMongoSortDirection(SortOrder.DESC)).toBe(-1);
});
});

عرض الملف

@@ -0,0 +1,7 @@
import { SortOrder } from '../enums/sort-order.enum';
export type MongoSortDirection = 1 | -1;
export const resolveMongoSortDirection = (
sortOrder?: SortOrder | null,
): MongoSortDirection => (sortOrder === SortOrder.ASC ? 1 : -1);

عرض الملف

@@ -0,0 +1,101 @@
const DEFAULT_SAMPLES = 48;
const scaleToRange = (values: number[]): number[] => {
if (!values.length) {
return [];
}
const max = Math.max(...values);
if (max <= 0) {
return values.map(() => 0);
}
return values.map((value) => Math.max(0, Math.min(100, Math.round((value / max) * 100))));
};
export const normalizeWaveformPeaks = (
input: number[] | undefined,
maxSamples = DEFAULT_SAMPLES,
): number[] => {
if (!input?.length) {
return [];
}
const cleaned = input
.map((value) => (Number.isFinite(value) ? Math.abs(value) : 0))
.filter((value) => value > 0);
if (!cleaned.length) {
return [];
}
if (cleaned.length <= maxSamples) {
return scaleToRange(cleaned);
}
const windowSize = cleaned.length / maxSamples;
const compressed: number[] = [];
for (let i = 0; i < maxSamples; i += 1) {
const start = Math.floor(i * windowSize);
const end = Math.min(cleaned.length, Math.floor((i + 1) * windowSize));
const slice = cleaned.slice(start, Math.max(start + 1, end));
const peak = Math.max(...slice);
compressed.push(peak);
}
return scaleToRange(compressed);
};
export const generateWaveformPeaksFromBuffer = (
buffer: Buffer,
samples = DEFAULT_SAMPLES,
): number[] => {
if (!buffer.length) {
return [];
}
const chunkSize = Math.max(1, Math.ceil(buffer.length / samples));
const peaks: number[] = [];
for (let offset = 0; offset < buffer.length; offset += chunkSize) {
const chunk = buffer.subarray(offset, Math.min(buffer.length, offset + chunkSize));
let total = 0;
let localPeak = 0;
for (const byte of chunk) {
const centered = Math.abs(byte - 128);
total += centered;
localPeak = Math.max(localPeak, centered);
}
const average = chunk.length ? total / chunk.length : 0;
peaks.push(Math.max(average, localPeak * 0.65));
}
return normalizeWaveformPeaks(peaks, samples);
};
export const generateWaveformPeaksFromSeed = (
seed: string,
samples = DEFAULT_SAMPLES,
): number[] => {
const source = seed.trim() || 'audio';
let hash = 2166136261;
for (let i = 0; i < source.length; i += 1) {
hash ^= source.charCodeAt(i);
hash = Math.imul(hash, 16777619);
}
const peaks: number[] = [];
let state = hash >>> 0;
for (let i = 0; i < samples; i += 1) {
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
const base = 18 + (state % 65);
const accent = i % 6 === 0 ? 18 : i % 3 === 0 ? 10 : 0;
peaks.push(base + accent);
}
return normalizeWaveformPeaks(peaks, samples);
};