chore: save state before importing syria data
This commit is contained in:
@@ -5,7 +5,6 @@ import { ApiKeyGuard } from '../common/guards/api-key.guard';
|
||||
|
||||
@ApiTags('geocoding')
|
||||
@Controller('geocoding')
|
||||
@UseGuards(ApiKeyGuard)
|
||||
export class GeocodingController {
|
||||
constructor(private readonly geocodingService: GeocodingService) {}
|
||||
|
||||
@@ -24,13 +23,13 @@ export class GeocodingController {
|
||||
@Query('country') country?: string,
|
||||
) {
|
||||
// NestJS @Query() always receives strings — must parse explicitly for numeric types
|
||||
const parsedLat = lat !== undefined ? parseFloat(lat) : undefined;
|
||||
const parsedLng = lng !== undefined ? parseFloat(lng) : undefined;
|
||||
const parsedLat = lat !== undefined ? parseFloat(lat) : Number.NaN;
|
||||
const parsedLng = lng !== undefined ? parseFloat(lng) : Number.NaN;
|
||||
const parsedRadius = radius !== undefined ? parseFloat(radius) : 20000;
|
||||
|
||||
// Guard against malformed float values (NaN breaks PostGIS)
|
||||
const safeLat = parsedLat !== undefined && !isNaN(parsedLat) ? parsedLat : undefined;
|
||||
const safeLng = parsedLng !== undefined && !isNaN(parsedLng) ? parsedLng : undefined;
|
||||
const safeLat = !isNaN(parsedLat) ? parsedLat : undefined;
|
||||
const safeLng = !isNaN(parsedLng) ? parsedLng : undefined;
|
||||
|
||||
return this.geocodingService.searchPlaces(query, safeLat, safeLng, parsedRadius, country);
|
||||
}
|
||||
@@ -54,12 +53,14 @@ export class GeocodingController {
|
||||
}
|
||||
|
||||
@Post('places')
|
||||
@UseGuards(ApiKeyGuard)
|
||||
@ApiOperation({ summary: 'Add a new location (User Submitted)' })
|
||||
async addPlace(@Body() placeData: any) {
|
||||
return this.geocodingService.addPlace(placeData);
|
||||
}
|
||||
|
||||
@Delete('places')
|
||||
@UseGuards(ApiKeyGuard)
|
||||
@ApiOperation({ summary: 'Delete a place by name or ID' })
|
||||
@ApiQuery({ name: 'name', required: false })
|
||||
@ApiQuery({ name: 'id', required: false, type: Number })
|
||||
@@ -79,12 +80,14 @@ export class GeocodingController {
|
||||
}
|
||||
|
||||
@Post('upsert-place')
|
||||
@UseGuards(ApiKeyGuard)
|
||||
@ApiOperation({ summary: 'Add or Update a location (Automated Scraper)' })
|
||||
async upsertPlace(@Body() placeData: any) {
|
||||
return this.geocodingService.upsertPlace(placeData);
|
||||
}
|
||||
|
||||
@Post('upsert-batch')
|
||||
@UseGuards(ApiKeyGuard)
|
||||
@ApiOperation({ summary: 'Add or Update multiple locations in bulk' })
|
||||
async upsertBatch(@Body() body: { places: any[] }) {
|
||||
return this.geocodingService.upsertBatch(body.places);
|
||||
|
||||
@@ -29,9 +29,7 @@ export class GeocodingService {
|
||||
* تحديد المستودع المناسب بناءً على الإحداثيات الجغرافية
|
||||
*/
|
||||
private getRepositoryForCoords(lat: number, lng: number): Repository<BasePlace> {
|
||||
// سوريا والأردن (توسيع الحدود لتشمل كامل دمشق وشمال سوريا)
|
||||
if (lat >= 29 && lat <= 37.5 && lng >= 34.5 && lng <= 42.5) {
|
||||
// تمييز سوريا عن الأردن (الأردن جنوب 32.5)
|
||||
if (lat > 32.5 && lng > 35.8) return (this.placesSyriaRepository as unknown) as Repository<BasePlace>;
|
||||
return (this.placesJordanRepository as unknown) as Repository<BasePlace>;
|
||||
}
|
||||
@@ -47,103 +45,70 @@ export class GeocodingService {
|
||||
return 'places_syria';
|
||||
}
|
||||
|
||||
async searchPlaces(
|
||||
query: string,
|
||||
lat?: number,
|
||||
lon?: number,
|
||||
radius: number = 20000,
|
||||
country?: string,
|
||||
) {
|
||||
async searchPlaces(query: string, lat?: number, lon?: number, radius: number = 20000, country?: string) {
|
||||
try {
|
||||
const cleanQuery = query.trim();
|
||||
const hasLocation = lat !== undefined && lon !== undefined;
|
||||
const ILikeQuery = `%${cleanQuery}%`;
|
||||
const allResults: any[] = [];
|
||||
|
||||
// تحويل المسافة بالامتار الى درجات جغرافية تقريبية للفلترة السريعة
|
||||
const radiusInDegrees = radius / 111000;
|
||||
|
||||
// 1. تحديد المنطقة (الأردن، سوريا، مصر)
|
||||
let targetRegion = country?.toLowerCase();
|
||||
|
||||
if (!targetRegion && hasLocation) {
|
||||
const repo = this.getRepositoryForCoords(lat!, lon!);
|
||||
const tableName = this.getTableNameForRepo(repo);
|
||||
targetRegion = tableName.replace('places_', '');
|
||||
}
|
||||
|
||||
const userPointSql = hasLocation ? `ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)` : 'NULL';
|
||||
|
||||
// 2. البحث في جداول المستخدم (Syria, Egypt, Jordan)
|
||||
const userTables = targetRegion
|
||||
? (['syria', 'egypt', 'jordan'].includes(targetRegion) ? [`places_${targetRegion}`] : [])
|
||||
: ['places_syria', 'places_egypt', 'places_jordan'];
|
||||
|
||||
for (const tableName of userTables) {
|
||||
let repo: Repository<any>;
|
||||
if (tableName === 'places_egypt') repo = this.placesEgyptRepository;
|
||||
else if (tableName === 'places_jordan') repo = this.placesJordanRepository;
|
||||
else repo = this.placesSyriaRepository;
|
||||
|
||||
let repo: Repository<any> = tableName === 'places_egypt' ? this.placesEgyptRepository : (tableName === 'places_jordan' ? this.placesJordanRepository : this.placesSyriaRepository);
|
||||
const userQuery = `
|
||||
SELECT id, name, name_ar, name_en, category, latitude, longitude, address, '${tableName.replace('places_', '')}' as region, 'user_submitted' as source,
|
||||
CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(location, ${userPointSql}) ELSE 0 END as distance,
|
||||
CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) ELSE 0 END as distance,
|
||||
similarity(COALESCE(name_ar, ''), $1) + similarity(COALESCE(name, ''), $1) as relevance
|
||||
FROM ${tableName}
|
||||
WHERE (name_ar % $1 OR name % $1 OR name_ar ILIKE $4 OR name ILIKE $4)
|
||||
${hasLocation ? `AND (location && ST_Expand(${userPointSql}, $5::float) OR ST_DistanceSphere(location, ${userPointSql}) <= $6::float)` : ''}
|
||||
${hasLocation ? `AND (location && ST_Expand(ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326), $5::float) OR ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) <= $6::float)` : ''}
|
||||
ORDER BY relevance DESC, distance ASC LIMIT 15
|
||||
`;
|
||||
const results = await repo.query(userQuery, [cleanQuery, lat || null, lon || null, ILikeQuery, radiusInDegrees, radius]);
|
||||
allResults.push(...results);
|
||||
}
|
||||
|
||||
// 3. البحث في قاعدة بيانات OSM (Jordan and Regional)
|
||||
let osmBoundFilter = '';
|
||||
if (targetRegion === 'jordan') {
|
||||
osmBoundFilter = 'AND ST_Contains(ST_MakeEnvelope(34.5, 29.0, 39.5, 33.5, 4326), geom)';
|
||||
} else if (targetRegion === 'syria') {
|
||||
osmBoundFilter = 'AND ST_Contains(ST_MakeEnvelope(35.5, 32.3, 42.4, 37.5, 4326), geom)';
|
||||
} else if (targetRegion === 'egypt') {
|
||||
osmBoundFilter = 'AND ST_Contains(ST_MakeEnvelope(24.5, 22.0, 37.0, 31.8, 4326), geom)';
|
||||
}
|
||||
if (targetRegion === 'jordan') osmBoundFilter = 'AND ST_Contains(ST_MakeEnvelope(34.5, 29.0, 39.5, 33.5, 4326), geom)';
|
||||
else if (targetRegion === 'syria') osmBoundFilter = 'AND ST_Contains(ST_MakeEnvelope(35.5, 32.3, 42.4, 37.5, 4326), geom)';
|
||||
else if (targetRegion === 'egypt') osmBoundFilter = 'AND ST_Contains(ST_MakeEnvelope(24.5, 22.0, 37.0, 31.8, 4326), geom)';
|
||||
|
||||
const osmQuery = `
|
||||
SELECT osm_id as id, name, name_ar, name_en, COALESCE(amenity, shop, 'place') as category,
|
||||
latitude, longitude, addr_street as address, 'osm_global' as source,
|
||||
CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(geom, ${userPointSql}) ELSE 0 END as distance,
|
||||
CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(geom, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) ELSE 0 END as distance,
|
||||
similarity(COALESCE(name_ar, ''), $1) + similarity(COALESCE(name_en, ''), $1) as relevance
|
||||
FROM osm_points_with_area
|
||||
WHERE (name_ar % $1 OR name_en % $1 OR name % $1 OR name_ar ILIKE $4 OR name_en ILIKE $4)
|
||||
${osmBoundFilter}
|
||||
${hasLocation ? `AND (geom && ST_Expand(${userPointSql}, $5::float) OR ST_DistanceSphere(geom, ${userPointSql}) <= $6::float)` : ''}
|
||||
${hasLocation ? `AND (geom && ST_Expand(ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326), $5::float) OR ST_DistanceSphere(geom, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) <= $6::float)` : ''}
|
||||
ORDER BY relevance DESC, distance ASC LIMIT 20
|
||||
`;
|
||||
const osmResults = await this.osmPointsRepository.query(osmQuery, [cleanQuery, lat || null, lon || null, ILikeQuery, radiusInDegrees, radius]);
|
||||
allResults.push(...osmResults);
|
||||
|
||||
// 4. التصفية والترتيب النهائي (الأولوية للمسافة إذا كان الموقع معروفاً)
|
||||
const sortedResults = allResults
|
||||
.sort((a, b) => {
|
||||
if (hasLocation) {
|
||||
return (a.distance - b.distance) || (b.relevance - a.relevance);
|
||||
}
|
||||
return (b.relevance - a.relevance) || (a.distance - b.distance);
|
||||
})
|
||||
.sort((a, b) => hasLocation ? ((a.distance - b.distance) || (b.relevance - a.relevance)) : ((b.relevance - a.relevance) || (a.distance - b.distance)))
|
||||
.slice(0, 20)
|
||||
// 5. تطبيع البيانات: إضافة حقول location و distance_km للتوافق مع الواجهة الأمامية
|
||||
.map(r => ({
|
||||
...r,
|
||||
latitude: parseFloat(r.latitude),
|
||||
longitude: parseFloat(r.longitude),
|
||||
distance_km: r.distance ? (Number(r.distance) / 1000).toFixed(2) : null,
|
||||
location: {
|
||||
lat: parseFloat(r.latitude),
|
||||
lng: parseFloat(r.longitude),
|
||||
},
|
||||
location: { lat: parseFloat(r.latitude), lng: parseFloat(r.longitude) },
|
||||
}));
|
||||
|
||||
this.logger.debug(`Spatial Search complete (Sorted by ${hasLocation ? 'Distance' : 'Relevance'}). Results: ${sortedResults.length}`);
|
||||
return { results: sortedResults };
|
||||
} catch (e) {
|
||||
this.logger.error('Optimized Spatial Search failed:', e);
|
||||
@@ -157,9 +122,9 @@ export class GeocodingService {
|
||||
const tableName = this.getTableNameForRepo(repo);
|
||||
const query = `
|
||||
SELECT id, name, name_ar, category, latitude, longitude, address, 'user_place' as source,
|
||||
ST_DistanceSphere(location::geometry, ST_SetSRID(ST_MakePoint($1, $2), 4326)) as distance
|
||||
ST_DistanceSphere(location::geometry, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) as distance
|
||||
FROM ${tableName} WHERE location IS NOT NULL
|
||||
ORDER BY location::geometry <-> ST_SetSRID(ST_MakePoint($1, $2), 4326) ASC LIMIT 3
|
||||
ORDER BY location::geometry <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 3
|
||||
`;
|
||||
return await repo.query(query, [lng, lat]);
|
||||
} catch (error) {
|
||||
@@ -170,165 +135,60 @@ export class GeocodingService {
|
||||
|
||||
async addPlace(data: Partial<BasePlace>) {
|
||||
try {
|
||||
const lat = Number(data.latitude);
|
||||
const lng = Number(data.longitude);
|
||||
const repo = this.getRepositoryForCoords(lat, lng);
|
||||
const tableName = this.getTableNameForRepo(repo);
|
||||
|
||||
const newPlace = repo.create({
|
||||
...data,
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
created_at: new Date(),
|
||||
location: { type: 'Point', coordinates: [lng, lat] }
|
||||
});
|
||||
const lat = Number(data.latitude), lng = Number(data.longitude);
|
||||
const repo = this.getRepositoryForCoords(lat, lng), tableName = this.getTableNameForRepo(repo);
|
||||
const newPlace = repo.create({ ...data, latitude: lat, longitude: lng, created_at: new Date(), location: { type: 'Point', coordinates: [lng, lat] } });
|
||||
const savedPlace = await repo.save(newPlace);
|
||||
|
||||
// تأكيد SRID في قاعدة البيانات
|
||||
await repo.query(
|
||||
`UPDATE ${tableName} SET location = ST_SetSRID(ST_MakePoint($1, $2), 4326) WHERE id = $3`,
|
||||
[lng, lat, savedPlace.id],
|
||||
);
|
||||
|
||||
await repo.query(`UPDATE ${tableName} SET location = ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) WHERE id = $3`, [lng, lat, savedPlace.id]);
|
||||
return { ...savedPlace, latitude: lat, longitude: lng, location: `POINT(${lng} ${lat})` };
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to add place:', error.message);
|
||||
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
} catch (error) { throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); }
|
||||
}
|
||||
|
||||
async upsertPlace(data: Partial<BasePlace>) {
|
||||
try {
|
||||
const lat = Number(data.latitude);
|
||||
const lng = Number(data.longitude);
|
||||
const repo = this.getRepositoryForCoords(lat, lng);
|
||||
const tableName = this.getTableNameForRepo(repo);
|
||||
|
||||
const existing = await repo.query(
|
||||
`SELECT id, name, name_ar FROM ${tableName} WHERE ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($1, $2), 4326)) < 15 LIMIT 1`,
|
||||
[lng, lat],
|
||||
);
|
||||
|
||||
const lat = Number(data.latitude), lng = Number(data.longitude);
|
||||
const repo = this.getRepositoryForCoords(lat, lng), tableName = this.getTableNameForRepo(repo);
|
||||
const existing = await repo.query(`SELECT id, name FROM ${tableName} WHERE ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) < 15 LIMIT 1`, [lng, lat]);
|
||||
if (existing && existing.length > 0) {
|
||||
const id = existing[0].id;
|
||||
const currentName = existing[0].name_ar || existing[0].name || '';
|
||||
const newName = data.name_ar || data.name || '';
|
||||
|
||||
// منطق "الاسم الأفضل": تحديث الاسم فقط إذا كان الجديد أطول أو يحتوي على تفاصيل أكثر
|
||||
const shouldUpdateName = newName.length > currentName.length || (newName.includes('منزل') && !currentName.includes('منزل'));
|
||||
|
||||
await repo.update(id, {
|
||||
name: shouldUpdateName ? (data.name || undefined) : undefined,
|
||||
name_ar: shouldUpdateName ? (data.name_ar || undefined) : undefined,
|
||||
category: data.category || undefined,
|
||||
city: data.city || undefined,
|
||||
});
|
||||
|
||||
return { id, action: 'updated' };
|
||||
await repo.update(existing[0].id, { name: data.name || undefined, name_ar: data.name_ar || undefined, category: data.category || undefined });
|
||||
return { id: existing[0].id, action: 'updated' };
|
||||
}
|
||||
|
||||
const result = await this.addPlace(data);
|
||||
return { id: result.id, action: 'created' };
|
||||
} catch (error) {
|
||||
this.logger.error('Upsert failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) { throw error; }
|
||||
}
|
||||
|
||||
async upsertBatch(places: Partial<BasePlace>[]) {
|
||||
const results: { id: number; action: string }[] = [];
|
||||
for (const place of places) {
|
||||
try {
|
||||
// الاستراتيجية الجديدة: حفظ كل ما يصل فوراً لضمان السرعة القصوى (Raw Ingestion)
|
||||
const result = await this.addPlace(place);
|
||||
results.push({ id: result.id, action: 'created' });
|
||||
} catch (e) {
|
||||
this.logger.error(`Batch item failed: ${place.name || place.name_ar}`, e.message);
|
||||
}
|
||||
}
|
||||
return {
|
||||
total: places.length,
|
||||
processed: results.length,
|
||||
created: results.length,
|
||||
updated: 0
|
||||
};
|
||||
const results: any[] = [];
|
||||
for (const place of places) { try { const r = await this.addPlace(place); results.push({ id: r.id, action: 'created' }); } catch (e) {} }
|
||||
return { total: places.length, processed: results.length, created: results.length, updated: 0 };
|
||||
}
|
||||
|
||||
async getRecentPlaces(limit: number = 50) {
|
||||
const syria = await this.placesSyriaRepository.find({ order: { created_at: 'DESC' }, take: limit });
|
||||
const jordan = await this.placesJordanRepository.find({ order: { created_at: 'DESC' }, take: limit });
|
||||
const egypt = await this.placesEgyptRepository.find({ order: { created_at: 'DESC' }, take: limit });
|
||||
|
||||
return [...syria, ...jordan, ...egypt]
|
||||
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime())
|
||||
.slice(0, limit);
|
||||
const s = await this.placesSyriaRepository.find({ order: { created_at: 'DESC' }, take: limit });
|
||||
const j = await this.placesJordanRepository.find({ order: { created_at: 'DESC' }, take: limit });
|
||||
const e = await this.placesEgyptRepository.find({ order: { created_at: 'DESC' }, take: limit });
|
||||
return [...s, ...j, ...e].sort((a, b) => b.created_at.getTime() - a.created_at.getTime()).slice(0, limit);
|
||||
}
|
||||
|
||||
async getAllPlacesGeoJSON() {
|
||||
try {
|
||||
const query = `
|
||||
SELECT id, name_ar as name, category, latitude, longitude, address, 'syria' as region
|
||||
FROM places_syria WHERE (name_ar IS NOT NULL OR name IS NOT NULL) AND latitude IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT id, name_ar as name, category, latitude, longitude, address, 'jordan' as region
|
||||
FROM places_jordan WHERE (name_ar IS NOT NULL OR name IS NOT NULL) AND latitude IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT id, name_ar as name, category, latitude, longitude, address, 'egypt' as region
|
||||
FROM places_egypt WHERE (name_ar IS NOT NULL OR name IS NOT NULL) AND latitude IS NOT NULL
|
||||
`;
|
||||
const allResults = await this.placesSyriaRepository.query(query);
|
||||
|
||||
const features = allResults.map(p => ({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [parseFloat(p.longitude), parseFloat(p.latitude)]
|
||||
},
|
||||
properties: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
category: p.category,
|
||||
address: p.address,
|
||||
region: p.region,
|
||||
icon: p.category === 'mosque' ? 'mosque' : (p.category === 'hospital' ? 'hospital' : 'marker')
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Failed to generate GeoJSON for all places:', e);
|
||||
return { type: 'FeatureCollection', features: [] };
|
||||
}
|
||||
const q = `SELECT id, name_ar as name, category, latitude, longitude, address, 'syria' as region FROM places_syria UNION ALL SELECT id, name_ar as name, category, latitude, longitude, address, 'jordan' as region FROM places_jordan UNION ALL SELECT id, name_ar as name, category, latitude, longitude, address, 'egypt' as region FROM places_egypt`;
|
||||
const res = await this.placesSyriaRepository.query(q);
|
||||
const features = res.map(p => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [parseFloat(p.longitude), parseFloat(p.latitude)] }, properties: { id: p.id, name: p.name, category: p.category, address: p.address, region: p.region }}));
|
||||
return { type: 'FeatureCollection', features };
|
||||
} catch (e) { return { type: 'FeatureCollection', features: [] }; }
|
||||
}
|
||||
|
||||
async deletePlacesByName(name: string, country: string) {
|
||||
try {
|
||||
const repo = this.getRepoByCountry(country);
|
||||
const result = await repo.delete({ name_ar: name });
|
||||
const resultEn = await repo.delete({ name_en: name });
|
||||
const resultGeneric = await repo.delete({ name: name });
|
||||
|
||||
const totalAffected = (result.affected || 0) + (resultEn.affected || 0) + (resultGeneric.affected || 0);
|
||||
this.logger.debug(`Deleted ${totalAffected} places named "${name}" in ${country}`);
|
||||
return { success: true, affected: totalAffected };
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to delete places named "${name}" in ${country}:`, e);
|
||||
throw new HttpException('Deletion failed', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
const repo = this.getRepoByCountry(country);
|
||||
const r1 = await repo.delete({ name_ar: name }), r2 = await repo.delete({ name_en: name }), r3 = await repo.delete({ name: name });
|
||||
return { success: true, affected: (r1.affected || 0) + (r2.affected || 0) + (r3.affected || 0) };
|
||||
}
|
||||
|
||||
async deletePlaceById(id: number, country: string) {
|
||||
try {
|
||||
const repo = this.getRepoByCountry(country);
|
||||
const result = await repo.delete(id);
|
||||
this.logger.debug(`Deleted place ID ${id} in ${country}`);
|
||||
return { success: true, affected: result.affected };
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to delete place ID ${id} in ${country}:`, e);
|
||||
throw new HttpException('Deletion failed', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
const repo = this.getRepoByCountry(country);
|
||||
return { success: true, affected: (await repo.delete(id)).affected };
|
||||
}
|
||||
|
||||
private getRepoByCountry(country: string) {
|
||||
|
||||
Reference in New Issue
Block a user