fix: resolve magic scan crash and implement context-aware AI languages
- Fixed SQL syntax error in magicScan caused by single quotes - Implemented dynamic locale-aware AI suggestions (Technical: EN, Custom Tags: Localized) - Updated Dexie schema to version 2 (added locale to pending_scans) - Fixed missing bottle_id in UploadQueue synchronization - Installed missing dexie dependencies via pnpm
This commit is contained in:
@@ -16,6 +16,8 @@
|
|||||||
"@supabase/supabase-js": "^2.39.0",
|
"@supabase/supabase-js": "^2.39.0",
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"canvas-confetti": "^1.9.2",
|
"canvas-confetti": "^1.9.2",
|
||||||
|
"dexie": "^4.2.1",
|
||||||
|
"dexie-react-hooks": "^4.2.0",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
"lucide-react": "^0.300.0",
|
"lucide-react": "^0.300.0",
|
||||||
"next": "14.2.23",
|
"next": "14.2.23",
|
||||||
|
|||||||
40
pnpm-lock.yaml
generated
40
pnpm-lock.yaml
generated
@@ -23,6 +23,12 @@ importers:
|
|||||||
canvas-confetti:
|
canvas-confetti:
|
||||||
specifier: ^1.9.2
|
specifier: ^1.9.2
|
||||||
version: 1.9.4
|
version: 1.9.4
|
||||||
|
dexie:
|
||||||
|
specifier: ^4.2.1
|
||||||
|
version: 4.2.1
|
||||||
|
dexie-react-hooks:
|
||||||
|
specifier: ^4.2.0
|
||||||
|
version: 4.2.0(@types/react@18.3.27)(dexie@4.2.1)(react@18.3.1)
|
||||||
heic2any:
|
heic2any:
|
||||||
specifier: ^0.0.4
|
specifier: ^0.0.4
|
||||||
version: 0.0.4
|
version: 0.0.4
|
||||||
@@ -1444,6 +1450,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
dexie-react-hooks@4.2.0:
|
||||||
|
resolution: {integrity: sha512-u7KqTX9JpBQK8+tEyA9X0yMGXlSCsbm5AU64N6gjvGk/IutYDpLBInMYEAEC83s3qhIvryFS+W+sqLZUBEvePQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=16'
|
||||||
|
dexie: '>=4.2.0-alpha.1 <5.0.0'
|
||||||
|
react: '>=16'
|
||||||
|
|
||||||
|
dexie@4.2.1:
|
||||||
|
resolution: {integrity: sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==}
|
||||||
|
|
||||||
didyoumean@1.2.2:
|
didyoumean@1.2.2:
|
||||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||||
|
|
||||||
@@ -4192,6 +4208,14 @@ snapshots:
|
|||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
|
dexie-react-hooks@4.2.0(@types/react@18.3.27)(dexie@4.2.1)(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.3.27
|
||||||
|
dexie: 4.2.1
|
||||||
|
react: 18.3.1
|
||||||
|
|
||||||
|
dexie@4.2.1: {}
|
||||||
|
|
||||||
didyoumean@1.2.2: {}
|
didyoumean@1.2.2: {}
|
||||||
|
|
||||||
dir-glob@3.0.1:
|
dir-glob@3.0.1:
|
||||||
@@ -4372,8 +4396,8 @@ snapshots:
|
|||||||
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3)
|
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
|
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
|
||||||
@@ -4392,7 +4416,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
|
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -4403,22 +4427,22 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
unrs-resolver: 1.11.1
|
unrs-resolver: 1.11.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3)
|
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@@ -4429,7 +4453,7 @@ snapshots:
|
|||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ interface CameraCaptureProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onSaveComplete }: CameraCaptureProps) {
|
export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onSaveComplete }: CameraCaptureProps) {
|
||||||
const { t } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const supabase = createClientComponentClient();
|
const supabase = createClientComponentClient();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -128,13 +128,14 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
|||||||
await db.pending_scans.add({
|
await db.pending_scans.add({
|
||||||
imageBase64: compressedBase64,
|
imageBase64: compressedBase64,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
provider: aiProvider
|
provider: aiProvider,
|
||||||
|
locale: locale
|
||||||
});
|
});
|
||||||
setIsQueued(true);
|
setIsQueued(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await magicScan(compressedBase64, aiProvider);
|
const response = await magicScan(compressedBase64, aiProvider, locale);
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
setAnalysisResult(response.data);
|
setAnalysisResult(response.data);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function UploadQueue() {
|
|||||||
const itemId = `scan-${item.id}`;
|
const itemId = `scan-${item.id}`;
|
||||||
setCurrentProgress({ id: itemId, status: 'Analysiere Scan...' });
|
setCurrentProgress({ id: itemId, status: 'Analysiere Scan...' });
|
||||||
try {
|
try {
|
||||||
const analysis = await analyzeBottle(item.imageBase64);
|
const analysis = await analyzeBottle(item.imageBase64, undefined, item.locale);
|
||||||
if (analysis.success && analysis.data) {
|
if (analysis.success && analysis.data) {
|
||||||
setCurrentProgress({ id: itemId, status: 'Speichere Flasche...' });
|
setCurrentProgress({ id: itemId, status: 'Speichere Flasche...' });
|
||||||
const save = await saveBottle(analysis.data, item.imageBase64, user.id);
|
const save = await saveBottle(analysis.data, item.imageBase64, user.id);
|
||||||
@@ -62,6 +62,7 @@ export default function UploadQueue() {
|
|||||||
try {
|
try {
|
||||||
const result = await saveTasting({
|
const result = await saveTasting({
|
||||||
...item.data,
|
...item.data,
|
||||||
|
bottle_id: item.bottle_id,
|
||||||
tasted_at: item.tasted_at
|
tasted_at: item.tasted_at
|
||||||
});
|
});
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface PendingScan {
|
|||||||
imageBase64: string;
|
imageBase64: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
provider?: 'gemini' | 'nebius';
|
provider?: 'gemini' | 'nebius';
|
||||||
|
locale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PendingTasting {
|
export interface PendingTasting {
|
||||||
@@ -45,8 +46,8 @@ export class WhiskyDexie extends Dexie {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('WhiskyVault');
|
super('WhiskyVault');
|
||||||
this.version(1).stores({
|
this.version(2).stores({
|
||||||
pending_scans: '++id, timestamp',
|
pending_scans: '++id, timestamp, locale',
|
||||||
pending_tastings: '++id, bottle_id, tasted_at',
|
pending_tastings: '++id, bottle_id, tasted_at',
|
||||||
cache_tags: 'id, category, name',
|
cache_tags: 'id, category, name',
|
||||||
cache_buddies: 'id, name'
|
cache_buddies: 'id, name'
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ Extract precise metadata. If the image is NOT a whisky bottle or if you are very
|
|||||||
If a value is not visible, use null.
|
If a value is not visible, use null.
|
||||||
Infer the 'Category' (e.g., Islay Single Malt) based on the Distillery if possible.
|
Infer the 'Category' (e.g., Islay Single Malt) based on the Distillery if possible.
|
||||||
Search specifically for a "Whiskybase ID" or "WB ID" on the label.
|
Search specifically for a "Whiskybase ID" or "WB ID" on the label.
|
||||||
|
IMPORTANT: Extract technical metadata (name, distillery, category) in English.
|
||||||
|
The 'suggested_custom_tags' MUST be localized in {LANGUAGE}.
|
||||||
|
|
||||||
PART 2: SENSORY ANALYSIS (AUTO-FILL)
|
PART 2: SENSORY ANALYSIS (AUTO-FILL)
|
||||||
Based on the identified bottle, select the most appropriate flavor tags.
|
Based on the identified bottle, select the most appropriate flavor tags.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { createHash } from 'crypto';
|
|||||||
import { trackApiUsage } from './track-api-usage';
|
import { trackApiUsage } from './track-api-usage';
|
||||||
import { checkCreditBalance, deductCredits } from './credit-service';
|
import { checkCreditBalance, deductCredits } from './credit-service';
|
||||||
|
|
||||||
export async function analyzeBottleNebius(base64Image: string, tags?: string[]): Promise<AnalysisResponse & { search_string?: string }> {
|
export async function analyzeBottleNebius(base64Image: string, tags?: string[], locale: string = 'de'): Promise<AnalysisResponse & { search_string?: string }> {
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = createServerActionClient({ cookies });
|
||||||
|
|
||||||
if (!process.env.NEBIUS_API_KEY) {
|
if (!process.env.NEBIUS_API_KEY) {
|
||||||
@@ -48,7 +48,10 @@ export async function analyzeBottleNebius(base64Image: string, tags?: string[]):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const instruction = GEMINI_SYSTEM_INSTRUCTION.replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available') + "\nAdditionally, generate a 'search_string' field for Whiskybase in this format: 'site:whiskybase.com [Distillery] [Name] [Vintage]'. Include this field in the JSON object.";
|
const instruction = GEMINI_SYSTEM_INSTRUCTION
|
||||||
|
.replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available')
|
||||||
|
.replace('{LANGUAGE}', locale === 'en' ? 'English' : 'German')
|
||||||
|
+ "\nAdditionally, generate a 'search_string' field for Whiskybase in this format: 'site:whiskybase.com [Distillery] [Name] [Vintage]'. Include this field in the JSON object.";
|
||||||
|
|
||||||
const response = await aiClient.chat.completions.create({
|
const response = await aiClient.chat.completions.create({
|
||||||
model: "Qwen/Qwen2.5-VL-72B-Instruct",
|
model: "Qwen/Qwen2.5-VL-72B-Instruct",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { createHash } from 'crypto';
|
|||||||
import { trackApiUsage } from './track-api-usage';
|
import { trackApiUsage } from './track-api-usage';
|
||||||
import { checkCreditBalance, deductCredits } from './credit-service';
|
import { checkCreditBalance, deductCredits } from './credit-service';
|
||||||
|
|
||||||
export async function analyzeBottle(base64Image: string, tags?: string[]): Promise<AnalysisResponse> {
|
export async function analyzeBottle(base64Image: string, tags?: string[], locale: string = 'de'): Promise<AnalysisResponse> {
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = createServerActionClient({ cookies });
|
||||||
|
|
||||||
if (!process.env.GEMINI_API_KEY) {
|
if (!process.env.GEMINI_API_KEY) {
|
||||||
@@ -48,7 +48,9 @@ export async function analyzeBottle(base64Image: string, tags?: string[]): Promi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const instruction = SYSTEM_INSTRUCTION.replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available');
|
const instruction = SYSTEM_INSTRUCTION
|
||||||
|
.replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available')
|
||||||
|
.replace('{LANGUAGE}', locale === 'en' ? 'English' : 'German');
|
||||||
|
|
||||||
const result = await geminiModel.generateContent([
|
const result = await geminiModel.generateContent([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { supabase } from '@/lib/supabase';
|
|||||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||||
import { AnalysisResponse, BottleMetadata } from '@/types/whisky';
|
import { AnalysisResponse, BottleMetadata } from '@/types/whisky';
|
||||||
|
|
||||||
export async function magicScan(base64Image: string, provider: 'gemini' | 'nebius' = 'gemini'): Promise<AnalysisResponse & { wb_id?: string }> {
|
export async function magicScan(base64Image: string, provider: 'gemini' | 'nebius' = 'gemini', locale: string = 'de'): Promise<AnalysisResponse & { wb_id?: string }> {
|
||||||
try {
|
try {
|
||||||
// 0. Fetch available tags for constrained generation
|
// 0. Fetch available tags for constrained generation
|
||||||
const systemTags = await getAllSystemTags();
|
const systemTags = await getAllSystemTags();
|
||||||
@@ -17,9 +17,9 @@ export async function magicScan(base64Image: string, provider: 'gemini' | 'nebiu
|
|||||||
// 1. AI Analysis
|
// 1. AI Analysis
|
||||||
let aiResponse: any;
|
let aiResponse: any;
|
||||||
if (provider === 'nebius') {
|
if (provider === 'nebius') {
|
||||||
aiResponse = await analyzeBottleNebius(base64Image, tagNames);
|
aiResponse = await analyzeBottleNebius(base64Image, tagNames, locale);
|
||||||
} else {
|
} else {
|
||||||
aiResponse = await analyzeBottle(base64Image, tagNames);
|
aiResponse = await analyzeBottle(base64Image, tagNames, locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!aiResponse.success || !aiResponse.data) {
|
if (!aiResponse.success || !aiResponse.data) {
|
||||||
@@ -38,7 +38,10 @@ export async function magicScan(base64Image: string, provider: 'gemini' | 'nebiu
|
|||||||
const { data: cacheHit } = await supabase
|
const { data: cacheHit } = await supabase
|
||||||
.from('global_products')
|
.from('global_products')
|
||||||
.select('wb_id')
|
.select('wb_id')
|
||||||
.textSearch('search_vector', `'${searchString}'`, { config: 'simple' })
|
.textSearch('search_vector', searchString, {
|
||||||
|
config: 'simple',
|
||||||
|
type: 'websearch'
|
||||||
|
})
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user