99
1010const config = require ( 'config' ) ;
1111const { sql } = require ( 'slonik' ) ;
12- const { clone } = require ( 'ramda' ) ;
12+ const { clone, omit } = require ( 'ramda' ) ;
1313const { runSequentially } = require ( '../../util/promise' ) ;
1414const { metricsTemplate } = require ( '../../data/analytics' ) ;
1515const oidc = require ( '../../util/oidc' ) ;
@@ -344,6 +344,33 @@ on forms."xmlFormId" = deleted_form_ids."xml_form_id" and
344344where forms."deletedAt" is null
345345group by forms."projectId"` ) ;
346346
347+ const maxGeoPerForm = ( ) => ( { oneFirst } ) => oneFirst ( sql `
348+ SELECT MAX(geo_count)
349+ FROM (
350+ SELECT COUNT(*) as geo_count, fd."formId"
351+ FROM submission_field_extract_geo_cache AS sc
352+ JOIN submission_defs AS sd
353+ ON sd.id = sc."submission_def_id"
354+ JOIN form_defs AS fd
355+ ON sd."formDefId" = fd.id
356+ GROUP BY fd."formId", sc.fieldhash
357+ ) as geo_counts` ) ;
358+
359+ const countFormsCreateUpdateEntitiesFromRepeats = ( ) => ( { all } ) => all ( sql `
360+ SELECT
361+ COUNT(DISTINCT CASE WHEN dfd.actions ? 'create' THEN f.id END) AS num_entity_create_forms,
362+ COUNT(DISTINCT CASE WHEN dfd.actions ? 'create' AND dfd."isRepeat" THEN f.id END) AS num_repeat_entity_create_forms,
363+ COUNT(DISTINCT CASE WHEN dfd.actions ? 'update' THEN f.id END) AS num_entity_update_forms,
364+ COUNT(DISTINCT CASE WHEN dfd.actions ? 'update' AND dfd."isRepeat" THEN f.id END) AS num_repeat_entity_update_forms,
365+ COUNT(DISTINCT CASE WHEN dfd.actions ? 'create' AND dfd.actions ? 'update' THEN f.id END) AS num_entity_create_update_forms,
366+ f."projectId"
367+ FROM forms AS f
368+ JOIN form_defs AS fd ON f."currentDefId" = fd."id"
369+ JOIN dataset_form_defs AS dfd ON dfd."formDefId" = fd.id
370+ WHERE f."deletedAt" IS NULL
371+ GROUP BY f."projectId"
372+ ORDER BY f."projectId"` ) ;
373+
347374// Submissions
348375const countSubmissions = ( ) => ( { all } ) => all ( sql `
349376select f."projectId", count(s.id) as total,
@@ -540,7 +567,7 @@ const getDatasets = () => ({ all }) => all(sql`
540567
541568const getDatasetEvents = ( ) => ( { all } ) => all ( sql `
542569SELECT
543- ds.id, ds."projectId",
570+ ds.id "datasetId" , ds."projectId",
544571 COUNT (*) num_bulk_create_events_total,
545572 SUM (CASE WHEN audits."loggedAt" >= ${ _cutoffDate } THEN 1 ELSE 0 END) num_bulk_create_events_recent,
546573 MAX (CAST(sources."details"->'count' AS integer)) AS biggest_bulk_upload
@@ -553,7 +580,7 @@ GROUP BY ds.id, ds."projectId"
553580
554581const getDatasetProperties = ( ) => ( { all } ) => all ( sql `
555582SELECT
556- ds.id, ds."projectId", COUNT(DISTINCT p.id) num_properties
583+ ds.id "datasetId" , ds."projectId", COUNT(DISTINCT p.id) num_properties
557584FROM datasets ds
558585 LEFT JOIN ds_properties p ON p."datasetId" = ds.id AND p."publishedAt" IS NOT NULL
559586WHERE ds."publishedAt" IS NOT NULL
@@ -661,6 +688,36 @@ const countOwnerOnlyDatasets = () => ({ oneFirst }) => oneFirst(sql`
661688 SELECT COUNT(1) FROM datasets where "ownerOnly" = true;
662689` ) ;
663690
691+ const countDatasetsWithGeometry = ( ) => ( { oneFirst } ) => oneFirst ( sql `
692+ SELECT COUNT(DISTINCT "datasetId") FROM ds_properties WHERE name = 'geometry';
693+ ` ) ;
694+
695+ const countEntitiesWithGeometry = ( ) => ( { all } ) => all ( sql `
696+ SELECT
697+ COUNT(*) AS total,
698+ COUNT(CASE WHEN e."createdAt" >= ${ _cutoffDate }
699+ THEN 1
700+ ELSE null
701+ END) AS recent,
702+ ds."projectId",
703+ ds.id as "datasetId"
704+ FROM entities AS e
705+ JOIN entity_defs AS ed ON ed."entityId" = e.id AND ed.current = true
706+ JOIN datasets AS ds ON ds.id = e."datasetId"
707+ WHERE ed.data ? 'geometry'
708+ AND e."deletedAt" IS NULL
709+ AND ds."publishedAt" IS NOT NULL
710+ GROUP BY ds.id, ds."projectId"` ) ;
711+
712+ const countEntityBulkDeletes = ( ) => ( { one } ) => one ( sql `
713+ SELECT
714+ count(*) AS total,
715+ count(CASE WHEN "loggedAt" >= ${ _cutoffDate }
716+ THEN 1
717+ ELSE null
718+ END) AS recent
719+ FROM audits WHERE action = 'entity.bulk.delete'` ) ;
720+
664721// Other
665722const getProjectsWithDescriptions = ( ) => ( { all } ) => all ( sql `
666723select id as "projectId", length(trim(description)) as description_length from projects where coalesce(trim(description),'')!=''` ) ;
@@ -676,6 +733,7 @@ const projectMetrics = () => (({ Analytics }) => runSequentially([
676733 Analytics . countFormsInStates ,
677734 Analytics . countFormsWebformsEnabled ,
678735 Analytics . countReusedFormIds ,
736+ Analytics . countFormsCreateUpdateEntitiesFromRepeats ,
679737 Analytics . countSubmissions ,
680738 Analytics . countSubmissionReviewStates ,
681739 Analytics . countSubmissionsEdited ,
@@ -684,11 +742,12 @@ const projectMetrics = () => (({ Analytics }) => runSequentially([
684742 Analytics . getProjectsWithDescriptions ,
685743 Analytics . getDatasets ,
686744 Analytics . getDatasetEvents ,
687- Analytics . getDatasetProperties
745+ Analytics . getDatasetProperties ,
746+ Analytics . countEntitiesWithGeometry ,
688747] ) . then ( ( [ userRoles , appUsers , deviceIds , pubLinks ,
689- forms , formGeoRepeats , formsEncrypt , formStates , webforms , reusedIds ,
748+ forms , formGeoRepeats , formsEncrypt , formStates , webforms , reusedIds , entityForms ,
690749 subs , subStates , subEdited , subComments , subUsers ,
691- projWithDesc , datasets , datasetEvents , datasetProperties ] ) => {
750+ projWithDesc , datasets , datasetEvents , datasetProperties , entitiesWithGeometry ] ) => {
692751 const projects = { } ;
693752
694753 // users
@@ -762,6 +821,11 @@ const projectMetrics = () => (({ Analytics }) => runSequentially([
762821 project . forms . num_reused_form_ids = row . total ;
763822 }
764823
824+ for ( const row of entityForms ) {
825+ const project = _getProject ( projects , row . projectId ) ;
826+ Object . assign ( project . forms , omit ( [ 'projectId' ] , row ) ) ;
827+ }
828+
765829 // submissions
766830 for ( const row of subs ) {
767831 const project = _getProject ( projects , row . projectId ) ;
@@ -798,21 +862,28 @@ const projectMetrics = () => (({ Analytics }) => runSequentially([
798862 project . submissions . num_submissions_from_web_users = { total : row . web_user_total , recent : row . web_user_recent } ;
799863 }
800864
865+
866+ // Helper function
867+ const findDataset = ( metric , dataset , defaultValue ) =>
868+ metric . find ( d => ( d . datasetId === dataset . id ) ) || defaultValue ;
869+
801870 // datasets
802871 for ( const row of datasets ) {
803872 const project = _getProject ( projects , row . projectId ) ;
804873
805874 // Additional dataset metrics are returned in a separate query. Look up the correct dataset/project row.
806- const eventsRow = datasetEvents . find ( d => ( d . projectId === row . projectId && d . id === row . id ) ) ||
807- { num_bulk_create_events_total : 0 , num_bulk_create_events_recent : 0 , biggest_bulk_upload : 0 } ;
875+ const eventsRow = findDataset ( datasetEvents , row , { num_bulk_create_events_total : 0 , num_bulk_create_events_recent : 0 , biggest_bulk_upload : 0 } ) ;
808876
809877 // Properties row
810- const propertiesRow = datasetProperties . find ( d => ( d . projectId === row . projectId && d . id === row . id ) ) ||
811- { num_properties : 0 } ;
878+ const propertiesRow = findDataset ( datasetProperties , row , { num_properties : 0 } ) ;
879+
880+ // Entities with geometry
881+ const entitiesWithGeometryRow = findDataset ( entitiesWithGeometry , row , { total : 0 , recent : 0 } ) ;
812882
813883 project . datasets . push ( {
814884 id : row . id ,
815885 num_properties : propertiesRow . num_properties ,
886+ num_entities_with_geometry : { total : entitiesWithGeometryRow . total , recent : entitiesWithGeometryRow . recent } ,
816887 num_creation_forms : row . num_creation_forms ,
817888 num_followup_forms : row . num_followup_forms ,
818889 num_entities : { total : row . num_entities_total , recent : row . num_entities_recent } ,
@@ -837,6 +908,7 @@ const projectMetrics = () => (({ Analytics }) => runSequentially([
837908 } ) ;
838909 }
839910
911+
840912 // other
841913 for ( const row of projWithDesc ) {
842914 const project = _getProject ( projects , row . projectId ) ;
@@ -852,6 +924,7 @@ const previewMetrics = () => (({ Analytics }) => runSequentially([
852924 Analytics . databaseSize ,
853925 Analytics . encryptedProjects ,
854926 Analytics . biggestForm ,
927+ Analytics . maxGeoPerForm ,
855928 Analytics . countAdmins ,
856929 Analytics . auditLogs ,
857930 Analytics . archivedProjects ,
@@ -870,12 +943,16 @@ const previewMetrics = () => (({ Analytics }) => runSequentially([
870943 Analytics . measureEntityProcessingTime ,
871944 Analytics . measureMaxEntityBranchTime ,
872945 Analytics . countOwnerOnlyDatasets ,
946+ Analytics . countDatasetsWithGeometry ,
947+ Analytics . countEntityBulkDeletes ,
873948 Analytics . projectMetrics
874- ] ) . then ( ( [ db , encrypt , bigForm , admins , audits ,
949+ ] ) . then ( ( [ db , encrypt , bigForm , maxGeo , admins , audits ,
875950 archived , managers , viewers , collectors ,
876951 caAttachments , caFailures , caRows , xmlDefs , blobFiles , resetFailedToPending ,
877952 oeBranches , oeInterruptedBranches , oeBacklogEvents , oeProcessingTime , oeBranchTime ,
878953 ownerOnlyDatasets ,
954+ datasetsWithGeometry ,
955+ entityBulkDeletes ,
879956 projMetrics ] ) => {
880957 const metrics = clone ( metricsTemplate ) ;
881958 // system
@@ -891,6 +968,7 @@ const previewMetrics = () => (({ Analytics }) => runSequentially([
891968 for ( const [ key , value ] of Object . entries ( encrypt ) )
892969 metrics . system . num_projects_encryption [ key ] = value ;
893970 metrics . system . num_questions_biggest_form = bigForm ;
971+ metrics . system . max_geo_per_form = maxGeo || 0 ;
894972 for ( const [ key , value ] of Object . entries ( admins ) )
895973 metrics . system . num_admins [ key ] = value ;
896974
@@ -930,6 +1008,12 @@ const previewMetrics = () => (({ Analytics }) => runSequentially([
9301008 // 2025.2.0 owner only entity lists/datasets
9311009 metrics . system . num_owner_only_datasets = ownerOnlyDatasets ;
9321010
1011+ // 2025.3.0 entity bulk delete audit logs
1012+ metrics . system . num_entity_bulk_deletes = entityBulkDeletes ;
1013+
1014+ // 2025.3.0 datasets with geometry property
1015+ metrics . system . num_datasets_with_geometry = datasetsWithGeometry || 0 ;
1016+
9331017 return metrics ;
9341018} ) ) ;
9351019
@@ -968,6 +1052,8 @@ module.exports = {
9681052 countFormFieldTypes,
9691053 countFormsInStates,
9701054 countFormsWebformsEnabled,
1055+ maxGeoPerForm,
1056+ countFormsCreateUpdateEntitiesFromRepeats,
9711057 countPublicLinks,
9721058 countReusedFormIds,
9731059 countSubmissions,
@@ -993,5 +1079,8 @@ module.exports = {
9931079 measureEntityProcessingTime,
9941080 measureElapsedEntityTime,
9951081 measureMaxEntityBranchTime,
996- countOwnerOnlyDatasets
1082+ countOwnerOnlyDatasets,
1083+ countDatasetsWithGeometry,
1084+ countEntitiesWithGeometry,
1085+ countEntityBulkDeletes
9971086} ;
0 commit comments