Prefer text fields in technical-form AI and add invoice payment methods.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-21 20:02:05 +03:30
co-authored by Cursor
parent f8b865b0aa
commit 503b70a690
7 changed files with 313 additions and 18 deletions
@@ -0,0 +1,51 @@
-- Meshkee CMS — invoice / template payment methods (نحوه ی پرداخت)
-- ---------------------------------------------------------------------------
-- template payment methods (duplicatable free-text)
-- ---------------------------------------------------------------------------
CREATE TABLE invoice_template_payment_methods (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
template_id BIGINT NOT NULL,
text TEXT NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT invoice_template_payment_methods_template_id_fkey
FOREIGN KEY (template_id) REFERENCES invoice_templates (id) ON DELETE CASCADE,
CONSTRAINT invoice_template_payment_methods_text_nonempty
CHECK (char_length(trim(text)) > 0)
);
CREATE INDEX idx_invoice_template_payment_methods_template_id
ON invoice_template_payment_methods (template_id, sort_order);
CREATE TRIGGER invoice_template_payment_methods_set_updated_at
BEFORE UPDATE ON invoice_template_payment_methods
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
-- ---------------------------------------------------------------------------
-- invoice payment methods (duplicatable free-text)
-- ---------------------------------------------------------------------------
CREATE TABLE invoice_payment_methods (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
invoice_id BIGINT NOT NULL,
text TEXT NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT invoice_payment_methods_invoice_id_fkey
FOREIGN KEY (invoice_id) REFERENCES invoices (id) ON DELETE CASCADE,
CONSTRAINT invoice_payment_methods_text_nonempty
CHECK (char_length(trim(text)) > 0)
);
CREATE INDEX idx_invoice_payment_methods_invoice_id
ON invoice_payment_methods (invoice_id, sort_order);
CREATE TRIGGER invoice_payment_methods_set_updated_at
BEFORE UPDATE ON invoice_payment_methods
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
+33 -5
View File
@@ -1380,11 +1380,12 @@ model InvoiceTemplate {
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
accounts InvoiceTemplateAccount[]
items InvoiceTemplateItem[]
keyPoints InvoiceTemplateKeyPoint[]
business Business? @relation(fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction)
invoices Invoice[]
accounts InvoiceTemplateAccount[]
items InvoiceTemplateItem[]
keyPoints InvoiceTemplateKeyPoint[]
paymentMethods InvoiceTemplatePaymentMethod[]
business Business? @relation(fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction)
invoices Invoice[]
@@index([ownerScope, sortOrder], map: "idx_invoice_templates_owner_scope")
@@map("invoice_templates")
@@ -1423,6 +1424,19 @@ model InvoiceTemplateKeyPoint {
@@map("invoice_template_key_points")
}
model InvoiceTemplatePaymentMethod {
id BigInt @id @default(autoincrement())
templateId BigInt @map("template_id")
text String
sortOrder Int @default(0) @map("sort_order")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
template InvoiceTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@index([templateId, sortOrder], map: "idx_invoice_template_payment_methods_template_id")
@@map("invoice_template_payment_methods")
}
model InvoiceTemplateAccount {
id BigInt @id @default(autoincrement())
templateId BigInt @map("template_id")
@@ -1458,6 +1472,7 @@ model Invoice {
accounts InvoiceAccount[]
items InvoiceItem[]
keyPoints InvoiceKeyPoint[]
paymentMethods InvoicePaymentMethod[]
business Business @relation("InvoiceBusiness", fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction)
invoiceTemplate InvoiceTemplate? @relation(fields: [invoiceTemplateId], references: [id], onUpdate: NoAction)
issuer User? @relation("InvoiceIssuer", fields: [issuedBy], references: [id], onUpdate: NoAction)
@@ -1504,6 +1519,19 @@ model InvoiceKeyPoint {
@@map("invoice_key_points")
}
model InvoicePaymentMethod {
id BigInt @id @default(autoincrement())
invoiceId BigInt @map("invoice_id")
text String
sortOrder Int @default(0) @map("sort_order")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@index([invoiceId, sortOrder], map: "idx_invoice_payment_methods_invoice_id")
@@map("invoice_payment_methods")
}
model InvoiceAccount {
id BigInt @id @default(autoincrement())
invoiceId BigInt @map("invoice_id")
+25 -4
View File
@@ -1379,7 +1379,7 @@ export class LegacyMigrateService {
.filter((m) => Number(m.size) > 0)
.slice(0, 12)
.map((m) => ({ id: m.id, fileName: m.file_name }));
const storagePrefix = await this.legacySourceS3.resolveBusinessPrefix(
const storagePrefix = await this.tryResolveStoragePrefix(
oldBizId,
oldSlug,
probeMedia,
@@ -1417,7 +1417,7 @@ export class LegacyMigrateService {
for (const media of articleMedia) {
const size = Number(media.size) || 0;
if (size <= 0) {
if (size <= 0 || !storagePrefix) {
imagesFailed += 1;
continue;
}
@@ -1611,7 +1611,7 @@ export class LegacyMigrateService {
.filter((m) => Number(m.size) > 0)
.slice(0, 12)
.map((m) => ({ id: m.id, fileName: m.file_name }));
const storagePrefix = await this.legacySourceS3.resolveBusinessPrefix(
const storagePrefix = await this.tryResolveStoragePrefix(
oldBizId,
oldSlug,
probeMedia,
@@ -1648,7 +1648,7 @@ export class LegacyMigrateService {
for (const media of itemMedia) {
const size = Number(media.size) || 0;
if (size <= 0) {
if (size <= 0 || !storagePrefix) {
imagesFailed += 1;
continue;
}
@@ -1978,6 +1978,27 @@ export class LegacyMigrateService {
};
}
private async tryResolveStoragePrefix(
oldBizId: number,
oldSlug: string | null,
probeMedia: Array<{ id: number; fileName: string }>,
): Promise<string | null> {
if (!probeMedia.length) return null;
try {
return await this.legacySourceS3.resolveBusinessPrefix(
oldBizId,
oldSlug,
probeMedia,
);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.warn(
`Legacy migrate: skipping image copy for business ${oldBizId}: ${message}`,
);
return null;
}
}
private async copyLegacyMedia(input: {
businessId: bigint;
oldMedia: OldMediaRow;
@@ -126,10 +126,12 @@ export class CategoryTechnicalFormAiService {
قوانین:
- بین ۵ تا ۱۱ فیلد کاربردی تولید کن که خریداران برای محصولات این دسته نیاز دارند (سیستم همیشه «سایر ویژگی‌ها» را اضافه می‌کند).
- از ترکیب text، textarea، select، multi_select و multi_value استفاده کن.
- فیلدهای select و multi_select باید ۳ تا ۸ گزینه واقعی داشته باشند.
- ترجیحاً از type=text استفاده کن تا فروشنده بتواند هر مقدار دلخواهی را آزادانه وارد کند.
- از select و multi_select حتی‌المقدور استفاده نکن. فقط وقتی مجموعه گزینه‌ها واقعاً ثابت، کوتاه و استاندارد است (مثلاً بله/خیر یا حداکثر چند گزینه قطعی) مجازند؛ برای ظرفیت، توان، ابعاد، مدل، جنس، رنگ آزاد و مشابه آن‌ها همیشه text بگذار.
- اگر بین text و select مردد هستی، حتماً text انتخاب کن.
- فیلدهای select و multi_select (در صورت استفاده) باید ۳ تا ۸ گزینه واقعی داشته باشند.
- multi_value یک لیست تکراری از مقادیر متنی آزاد است (بدون گزینه) برای مقادیر کوتاه چندتایی.
- text برای مقادیر کوتاه (ابعاد، وزن، سال مدل).
- text برای مقادیر کوتاه (ابعاد، وزن، سال مدل، ظرفیت، توان).
- textarea برای مشخصات طولانی‌تر (ویژگی‌ها، دستورالعمل نگهداری).
- اگر برای این دسته مناسب است، فیلدهای «ابعاد» و «وزن» را به‌صورت text اضافه کن (واحد متریک رایج ایران را در برچسب بیاور، مثلاً سانتی‌متر / کیلوگرم). اگر برای دسته بی‌معناست، اضافه نکن.
- مشخصات مهم خریدار را الزامی علامت بزن.
+31 -1
View File
@@ -57,6 +57,12 @@ export class InvoiceKeyPointInputDto {
text!: string;
}
export class InvoicePaymentMethodInputDto {
@IsString()
@MinLength(1)
text!: string;
}
export class InvoiceAccountInputDto {
@IsString()
@MinLength(1)
@@ -114,6 +120,12 @@ export class CreateInvoiceDto {
@Type(() => InvoiceKeyPointInputDto)
keyPoints?: InvoiceKeyPointInputDto[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => InvoicePaymentMethodInputDto)
paymentMethods?: InvoicePaymentMethodInputDto[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@@ -134,7 +146,7 @@ export class UpdateInvoiceStatusDto {
notes?: string;
}
/** Full content replace for an existing invoice (items / key points / accounts). */
/** Full content replace for an existing invoice (items / key points / payment methods / accounts). */
export class UpdateInvoiceDto {
@IsOptional()
@IsString()
@@ -169,6 +181,12 @@ export class UpdateInvoiceDto {
@Type(() => InvoiceKeyPointInputDto)
keyPoints?: InvoiceKeyPointInputDto[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => InvoicePaymentMethodInputDto)
paymentMethods?: InvoicePaymentMethodInputDto[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@@ -337,6 +355,12 @@ export class CreateInvoiceTemplateDto {
@Type(() => InvoiceKeyPointInputDto)
keyPoints?: InvoiceKeyPointInputDto[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => InvoicePaymentMethodInputDto)
paymentMethods?: InvoicePaymentMethodInputDto[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@@ -373,6 +397,12 @@ export class UpdateInvoiceTemplateDto {
@Type(() => InvoiceKeyPointInputDto)
keyPoints?: InvoiceKeyPointInputDto[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => InvoicePaymentMethodInputDto)
paymentMethods?: InvoicePaymentMethodInputDto[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
+113
View File
@@ -16,6 +16,7 @@ import {
InvoiceAccountInputDto,
InvoiceItemInputDto,
InvoiceKeyPointInputDto,
InvoicePaymentMethodInputDto,
InvoiceTemplateItemInputDto,
ListInvoicesDto,
UpdateInvoiceDto,
@@ -250,12 +251,14 @@ export class InvoicesService {
user: { select: { id: true, firstName: true, lastName: true, cellNumber: true } },
items: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
keyPoints: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
paymentMethods: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
accounts: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
};
private readonly invoiceTemplateInclude = {
items: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
keyPoints: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
paymentMethods: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
accounts: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
};
@@ -271,6 +274,18 @@ export class InvoicesService {
};
}
private serializePaymentMethod(row: {
id: bigint;
text: string;
sortOrder: number;
}) {
return {
id: row.id.toString(),
text: row.text,
sortOrder: row.sortOrder,
};
}
private serializeAccount(row: {
id: bigint;
bankName: string;
@@ -343,6 +358,11 @@ export class InvoicesService {
text: string;
sortOrder: number;
}>;
paymentMethods?: Array<{
id: bigint;
text: string;
sortOrder: number;
}>;
accounts?: Array<{
id: bigint;
bankName: string;
@@ -372,6 +392,10 @@ export class InvoicesService {
includeNested && row.keyPoints
? row.keyPoints.map((kp) => this.serializeKeyPoint(kp))
: undefined,
paymentMethods:
includeNested && row.paymentMethods
? row.paymentMethods.map((pm) => this.serializePaymentMethod(pm))
: undefined,
accounts:
includeNested && row.accounts
? row.accounts.map((acc) => this.serializeAccount(acc))
@@ -488,6 +512,11 @@ export class InvoicesService {
text: string;
sortOrder: number;
}>;
paymentMethods?: Array<{
id: bigint;
text: string;
sortOrder: number;
}>;
accounts?: Array<{
id: bigint;
bankName: string;
@@ -564,6 +593,10 @@ export class InvoicesService {
includeNested && row.keyPoints
? row.keyPoints.map((kp) => this.serializeKeyPoint(kp))
: undefined,
paymentMethods:
includeNested && row.paymentMethods
? row.paymentMethods.map((pm) => this.serializePaymentMethod(pm))
: undefined,
accounts:
includeNested && row.accounts
? row.accounts.map((acc) => this.serializeAccount(acc))
@@ -581,6 +614,14 @@ export class InvoicesService {
return { text };
}
private normalizePaymentMethodInput(paymentMethod: InvoicePaymentMethodInputDto) {
const text = paymentMethod.text.trim();
if (!text) {
throw new BadRequestException('Each payment method requires text');
}
return { text };
}
private normalizeAccountInput(account: InvoiceAccountInputDto) {
const bankName = account.bankName.trim();
if (!bankName) {
@@ -793,6 +834,9 @@ export class InvoicesService {
const items = dto.items.map((item) => this.normalizeTemplateItemInput(item));
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
const paymentMethods = (dto.paymentMethods ?? []).map((pm) =>
this.normalizePaymentMethodInput(pm),
);
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
const row = await this.prisma.invoiceTemplate.create({
@@ -819,6 +863,12 @@ export class InvoicesService {
sortOrder: index,
})),
},
paymentMethods: {
create: paymentMethods.map((pm, index) => ({
text: pm.text,
sortOrder: index,
})),
},
accounts: {
create: accounts.map((acc, index) => ({
bankName: acc.bankName,
@@ -856,6 +906,10 @@ export class InvoicesService {
dto.keyPoints !== undefined
? dto.keyPoints.map((kp) => this.normalizeKeyPointInput(kp))
: null;
const replacePaymentMethods =
dto.paymentMethods !== undefined
? dto.paymentMethods.map((pm) => this.normalizePaymentMethodInput(pm))
: null;
const replaceAccounts =
dto.accounts !== undefined
? dto.accounts.map((acc) => this.normalizeAccountInput(acc))
@@ -868,6 +922,9 @@ export class InvoicesService {
if (replaceKeyPoints !== null) {
await tx.invoiceTemplateKeyPoint.deleteMany({ where: { templateId } });
}
if (replacePaymentMethods !== null) {
await tx.invoiceTemplatePaymentMethod.deleteMany({ where: { templateId } });
}
if (replaceAccounts !== null) {
await tx.invoiceTemplateAccount.deleteMany({ where: { templateId } });
}
@@ -905,6 +962,16 @@ export class InvoicesService {
},
}
: {}),
...(replacePaymentMethods !== null
? {
paymentMethods: {
create: replacePaymentMethods.map((pm, index) => ({
text: pm.text,
sortOrder: index,
})),
},
}
: {}),
...(replaceAccounts !== null
? {
accounts: {
@@ -1116,6 +1183,9 @@ export class InvoicesService {
const items = dto.items.map((item) => this.normalizeTemplateItemInput(item));
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
const paymentMethods = (dto.paymentMethods ?? []).map((pm) =>
this.normalizePaymentMethodInput(pm),
);
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
const row = await this.prisma.invoiceTemplate.create({
@@ -1143,6 +1213,12 @@ export class InvoicesService {
sortOrder: index,
})),
},
paymentMethods: {
create: paymentMethods.map((pm, index) => ({
text: pm.text,
sortOrder: index,
})),
},
accounts: {
create: accounts.map((acc, index) => ({
bankName: acc.bankName,
@@ -1183,6 +1259,10 @@ export class InvoicesService {
dto.keyPoints !== undefined
? dto.keyPoints.map((kp) => this.normalizeKeyPointInput(kp))
: null;
const replacePaymentMethods =
dto.paymentMethods !== undefined
? dto.paymentMethods.map((pm) => this.normalizePaymentMethodInput(pm))
: null;
const replaceAccounts =
dto.accounts !== undefined
? dto.accounts.map((acc) => this.normalizeAccountInput(acc))
@@ -1195,6 +1275,9 @@ export class InvoicesService {
if (replaceKeyPoints !== null) {
await tx.invoiceTemplateKeyPoint.deleteMany({ where: { templateId } });
}
if (replacePaymentMethods !== null) {
await tx.invoiceTemplatePaymentMethod.deleteMany({ where: { templateId } });
}
if (replaceAccounts !== null) {
await tx.invoiceTemplateAccount.deleteMany({ where: { templateId } });
}
@@ -1232,6 +1315,16 @@ export class InvoicesService {
},
}
: {}),
...(replacePaymentMethods !== null
? {
paymentMethods: {
create: replacePaymentMethods.map((pm, index) => ({
text: pm.text,
sortOrder: index,
})),
},
}
: {}),
...(replaceAccounts !== null
? {
accounts: {
@@ -1304,6 +1397,7 @@ export class InvoicesService {
: null,
items: serialized.items,
keyPoints: serialized.keyPoints,
paymentMethods: serialized.paymentMethods,
accounts: serialized.accounts,
subtotal: serialized.subtotal,
total: serialized.total,
@@ -1465,6 +1559,9 @@ export class InvoicesService {
const items = dto.items.map((item) => this.normalizeItemInput(item));
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
const paymentMethods = (dto.paymentMethods ?? []).map((pm) =>
this.normalizePaymentMethodInput(pm),
);
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
const invoiceTemplateId = dto.invoiceTemplateId?.trim()
? BigInt(dto.invoiceTemplateId)
@@ -1514,6 +1611,12 @@ export class InvoicesService {
sortOrder: index,
})),
},
paymentMethods: {
create: paymentMethods.map((pm, index) => ({
text: pm.text,
sortOrder: index,
})),
},
accounts: {
create: accounts.map((acc, index) => ({
bankName: acc.bankName,
@@ -1600,6 +1703,9 @@ export class InvoicesService {
const items = dto.items.map((item) => this.normalizeItemInput(item));
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
const paymentMethods = (dto.paymentMethods ?? []).map((pm) =>
this.normalizePaymentMethodInput(pm),
);
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
let userId: bigint | undefined;
@@ -1629,6 +1735,7 @@ export class InvoicesService {
const row = await this.prisma.$transaction(async (tx) => {
await tx.invoiceItem.deleteMany({ where: { invoiceId } });
await tx.invoiceKeyPoint.deleteMany({ where: { invoiceId } });
await tx.invoicePaymentMethod.deleteMany({ where: { invoiceId } });
await tx.invoiceAccount.deleteMany({ where: { invoiceId } });
return tx.invoice.update({
@@ -1657,6 +1764,12 @@ export class InvoicesService {
sortOrder: index,
})),
},
paymentMethods: {
create: paymentMethods.map((pm, index) => ({
text: pm.text,
sortOrder: index,
})),
},
accounts: {
create: accounts.map((acc, index) => ({
bankName: acc.bankName,
+55 -5
View File
@@ -60,13 +60,23 @@ export class LegacySourceS3Service implements OnModuleDestroy {
const slug = slugHint.trim();
add(`${slug}_${oldBusinessId}`);
add(`${slug.replace(/-/g, '')}_${oldBusinessId}`);
add(`${slug.replace(/_/g, '')}_${oldBusinessId}`);
add(`${slug.replace(/_/g, '-')}_${oldBusinessId}`);
}
const listed = await this.listTopLevelPrefixes();
for (const prefix of listed) {
if (prefix.endsWith(`_${oldBusinessId}`)) {
add(prefix);
// ListBucket may be denied on the old bucket; slug candidates + HeadObject still work.
try {
const listed = await this.listTopLevelPrefixes();
for (const prefix of listed) {
if (prefix.endsWith(`_${oldBusinessId}`)) {
add(prefix);
}
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.warn(
`Legacy S3 list prefixes failed for business ${oldBusinessId}; using slug candidates only: ${message}`,
);
}
if (!candidates.length) {
@@ -75,7 +85,8 @@ export class LegacySourceS3Service implements OnModuleDestroy {
);
}
const sample = probes.slice(0, 12);
// Spread probes so early missing files don't hide a valid prefix.
const sample = this.pickProbeSample(probes, 12);
let bestPrefix = candidates[0];
let bestHits = -1;
@@ -96,6 +107,16 @@ export class LegacySourceS3Service implements OnModuleDestroy {
}
}
if (bestHits <= 0 && sample.length > 0) {
for (const prefix of candidates) {
if (await this.prefixHasAnyObject(prefix)) {
bestPrefix = prefix;
bestHits = 1;
break;
}
}
}
if (bestHits <= 0 && sample.length > 0) {
throw new ServiceUnavailableException(
`Old S3 files not found for business ${oldBusinessId} (tried: ${candidates.join(', ')}).`,
@@ -106,6 +127,35 @@ export class LegacySourceS3Service implements OnModuleDestroy {
return bestPrefix;
}
private pickProbeSample<T>(items: T[], n: number): T[] {
if (items.length <= n) return [...items];
const out: T[] = [];
const seen = new Set<number>();
for (let i = 0; i < n; i++) {
const idx = Math.floor((i * (items.length - 1)) / Math.max(n - 1, 1));
if (seen.has(idx)) continue;
seen.add(idx);
out.push(items[idx]);
}
return out;
}
private async prefixHasAnyObject(prefix: string): Promise<boolean> {
const { client, bucket } = this.getClient();
try {
const res = await client.send(
new ListObjectsV2Command({
Bucket: bucket,
Prefix: `${prefix}/`,
MaxKeys: 1,
}),
);
return (res.Contents?.length ?? 0) > 0;
} catch {
return false;
}
}
async getObjectBuffer(key: string): Promise<Buffer> {
const { client, bucket } = this.getClient();
try {