Skip to content

Commit

Permalink
feat: update removing of adoption
Browse files Browse the repository at this point in the history
  • Loading branch information
alexey-yarmosh committed Feb 27, 2025
1 parent c3386b6 commit d4f1696
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 18 deletions.
12 changes: 12 additions & 0 deletions src/extensions/endpoints/adoption-code/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type SendCodeResponse = {
longitude: number;
asn: number;
network: string;
isIPv4Supported: boolean;
isIPv6Supported: boolean;
}

export type AdoptedProbe = {
Expand All @@ -52,6 +54,8 @@ export type AdoptedProbe = {
longitude: number | null;
asn: number | null;
network: string | null;
isIPv4Supported: boolean;
isIPv6Supported: boolean;
};

const InvalidCodeError = createError('INVALID_PAYLOAD_ERROR', 'Invalid code', 400);
Expand Down Expand Up @@ -127,6 +131,8 @@ export default defineEndpoint((router, context) => {
longitude: null,
asn: null,
network: null,
isIPv4Supported: false,
isIPv6Supported: false,
});

if (env.ENABLE_E2E_MOCKS === true) {
Expand All @@ -148,6 +154,8 @@ export default defineEndpoint((router, context) => {
longitude: -1.53,
asn: 3302,
network: 'e2e network provider',
isIPv4Supported: true,
isIPv6Supported: false,
});

res.send('Code was sent to the probe.');
Expand Down Expand Up @@ -179,6 +187,8 @@ export default defineEndpoint((router, context) => {
longitude: data.longitude,
asn: data.asn,
network: data.network,
isIPv4Supported: data.isIPv4Supported,
isIPv6Supported: data.isIPv6Supported,
});

res.send('Code was sent to the probe.');
Expand Down Expand Up @@ -256,6 +266,8 @@ export default defineEndpoint((router, context) => {
asn: probe.asn,
network: probe.network,
lastSyncDate: new Date(),
isIPv4Supported: probe.isIPv4Supported,
isIPv6Supported: probe.isIPv6Supported,
});
} catch (error: unknown) {
logger.error(error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,22 @@ export const createAdoptedProbe = async (req: Request, probe: AdoptedProbe, cont
network: probe.network,
userId: req.accountability.user,
lastSyncDate: new Date(),
isIPv4Supported: probe.isIPv4Supported,
isIPv6Supported: probe.isIPv6Supported,
};

const existingProbe = (await itemsService.readByQuery({
ip: probe.ip,
const existingProbe = (await itemsService.readByQuery({ // TODO: handle adoption by alt ip
filter: {
ip: probe.ip,
},
}) as { id: string }[])[0];

let id: string;

if (existingProbe) {
id = await itemsService.updateOne(existingProbe.id, adoption);
id = await itemsService.updateOne(existingProbe.id, adoption, { emitEvents: false });
} else {
id = await itemsService.createOne(adoption);
id = await itemsService.createOne(adoption, { emitEvents: false });
}

return [ id, name ] as const;
Expand Down Expand Up @@ -75,9 +79,13 @@ const getDefaultProbeName = async (req: Request, probe: AdoptedProbe, context: E
};

export const findAdoptedProbesByIp = async (ip: string, { database }: EndpointExtensionContext) => {
const probes = await database('gp_probes')
.whereRaw('JSON_CONTAINS(altIps, ?)', [ `"${ip}"` ])
.orWhere('ip', ip);
const probes = await database('gp_probes').whereRaw(`
(
ip = ?
OR JSON_CONTAINS(altIps, ?)
)
AND userId IS NOT NULL
`, [ ip, `"${ip}"` ]);

return probes;
};
11 changes: 9 additions & 2 deletions src/extensions/hooks/adopted-probe/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { createError } from '@directus/errors';
import { defineHook } from '@directus/extensions-sdk';
import { resetCustomCityData, updateCustomCityData } from './update-metadata.js';
import { resetCustomCityData, resetUserDefinedData, updateCustomCityData } from './update-metadata.js';
import { validateCity, validateTags } from './validate-fields.js';

export type Probe = {
name: string | null;
city: string | null;
state: string | null;
latitude: string | null;
longitude: string | null;
country: string | null;
countryOfCustomCity: string | null;
isCustomCity: boolean;
tags: {value: string; prefix: string}[] | null;
userId: string | null;
Expand All @@ -35,7 +37,7 @@ export default defineHook(({ filter, action }, context) => {
}
});

// State, latitude and longitude are updated in a separate hook, because user operation doesn't have permission to edit them.
// State, latitude and longitude are updated in action hook, because user operation doesn't have permissions to edit them.
action('gp_probes.items.update', async ({ keys, payload }) => {
const fields = payload as Fields;

Expand All @@ -44,5 +46,10 @@ export default defineHook(({ filter, action }, context) => {
} else if (fields.city === null) {
await resetCustomCityData(fields, keys, context);
}

// In case of removing adoption, reset all user affected fields.
if (fields.userId === null) {
await resetUserDefinedData(fields, keys, context);
}
});
});
19 changes: 19 additions & 0 deletions src/extensions/hooks/adopted-probe/src/update-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,22 @@ export const updateCustomCityData = async (_fields: Fields, keys: string[], { se
emitEvents: false,
});
};

export const resetUserDefinedData = async (_fields: Fields, keys: string[], { services, database, getSchema }: HookExtensionContext) => {
const { ItemsService } = services;

const adoptedProbesService = new ItemsService('gp_probes', {
database,
schema: await getSchema(),
});

await adoptedProbesService.updateMany(keys, {
name: null,
userId: null,
tags: [],
isCustomCity: false,
countryOfCustomCity: null,
}, {
emitEvents: false,
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { getUserPermissions } from '../migration-utils.js';

const DIRECTUS_URL = process.env.DIRECTUS_URL;
const ADMIN_ACCESS_TOKEN = process.env.ADMIN_ACCESS_TOKEN;
const COLLECTION_NAME = 'gp_probes';

export async function up () {
const { updatePermissions, deletePermissions } = await getUserPermissions(COLLECTION_NAME);
await removeDeletePermissions(deletePermissions);
await allowResettingUserId(updatePermissions);
console.log(`Changed the way adoption is removed in: ${COLLECTION_NAME}`);
}

export async function removeDeletePermissions (permissionsObj) {
if (!permissionsObj) {
throw new Error(`Permissions object is empty.`);
}

if (!permissionsObj.id) {
console.error(permissionsObj);
throw new Error(`Permissions ID is missing. This may happen when there are multiple rows for the same permission type.`);
}

const URL = `${DIRECTUS_URL}/permissions/${permissionsObj.id}?access_token=${ADMIN_ACCESS_TOKEN}`;

const response = await fetch(URL, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
}).then((response) => {
if (!response.ok) {
throw new Error(`Fetch request failed. Status: ${response.status}`);
}

return response.text();
});
return response.data;
}

export async function allowResettingUserId (permissionsObj) {
if (!permissionsObj) {
throw new Error(`Permissions object is empty.`);
}

if (!permissionsObj.id) {
console.error(permissionsObj);
throw new Error(`Permissions ID is missing. This may happen when there are multiple rows for the same permission type.`);
}

const URL = `${DIRECTUS_URL}/permissions/${permissionsObj.id}?access_token=${ADMIN_ACCESS_TOKEN}`;

const response = await fetch(URL, {
method: 'PATCH',
body: JSON.stringify({
...permissionsObj,
fields: [
...permissionsObj.fields,
'userId',
],
validation: {
_and: [
{
userId: {
_null: true,
},
},
],
},
}),
headers: {
'Content-Type': 'application/json',
},
}).then((response) => {
if (!response.ok) {
throw new Error(`Fetch request failed. Status: ${response.status}`);
}

return response.json();
});
return response.data;
}

export async function down () {
console.log('There is no down operation for that migration.');
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@

import type { OperationContext } from '@directus/extensions';
import { getOfflineAdoptions, getExistingNotifications, notifyProbes, deleteProbes } from '../repositories/directus.js';
import { getOfflineAdoptions, getExistingNotifications, notifyProbes, removeAdoption } from '../repositories/directus.js';
import type { AdoptedProbe } from '../types.js';

export const NOTIFY_AFTER_DAYS = 2;
export const REMOVE_AFTER_DAYS = 30;


export const removeExpiredProbes = async (context: OperationContext): Promise<{ notifiedIds: string[], removedIds: string[] }> => {
export const removeExpiredAdoptions = async (context: OperationContext): Promise<{ notifiedIds: string[], removedIds: string[] }> => {
const offlineAdoptedProbes = await getOfflineAdoptions(context);
const probesToNotify = [];
const probesToDelete = [];
const probesToRemoveAdoption = [];

for (const probe of offlineAdoptedProbes) {
if (isExpired(probe.lastSyncDate, REMOVE_AFTER_DAYS)) {
probesToDelete.push(probe);
probesToRemoveAdoption.push(probe);
} else if (isExpired(probe.lastSyncDate, NOTIFY_AFTER_DAYS)) {
probesToNotify.push(probe);
}
}

const probesWithoutNotifications = await excludeAlreadyNotifiedProbes(probesToNotify, context);
const notifiedIds = await notifyProbes(probesWithoutNotifications, context);
const removedIds = await deleteProbes(probesToDelete, context);
const removedIds = await removeAdoption(probesToRemoveAdoption, context);
return { notifiedIds, removedIds };
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { OperationContext } from '@directus/extensions';
import { defineOperationApi } from '@directus/extensions-sdk';
import { removeExpiredProbes } from './actions/remove-expired-probes.js';
import { removeExpiredAdoptions } from './actions/remove-expired-probes.js';

export default defineOperationApi({
id: 'remove-expired-adoptions-cron-handler',
handler: async (_operationData, context) => {
const { removedIds, notifiedIds } = await removeExpiredProbes(context as OperationContext);
const { removedIds, notifiedIds } = await removeExpiredAdoptions(context as OperationContext);

return `Removed probes with ids: ${removedIds.toString() || '[]'}. Notified probes with ids: ${notifiedIds.toString() || '[]'}.`;
return `Removed adoptions for probes: ${removedIds.toString() || '[]'}. Notified adoptions with ids: ${notifiedIds.toString() || '[]'}.`;
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const notifyProbes = async (probes: AdoptedProbe[], { services, database,
return probes.map(probe => probe.id);
};

export const deleteProbes = async (probes: AdoptedProbe[], { services, database, getSchema }: OperationContext): Promise<string[]> => {
export const removeAdoption = async (probes: AdoptedProbe[], { services, database, getSchema }: OperationContext): Promise<string[]> => {
const { NotificationsService, ItemsService } = services;

if (!probes.length) {
Expand Down Expand Up @@ -113,6 +113,7 @@ export const deleteProbes = async (probes: AdoptedProbe[], { services, database,
});

const result = await probesService.updateByQuery({ filter: { id: { _in: probes.map(probe => probe.id) } } }, {
name: null,
userId: null,
tags: [],
isCustomCity: false,
Expand Down

0 comments on commit d4f1696

Please sign in to comment.