Email Reports to Specific People

Hi Elton,

I’m not trying to bug you with repeated questions about this, but I’ve discovered something which could be pertinent: the emailing process works currently and correctly attaches the report to the email if it is not a “staged” report. When it is a staged report, it returns a very tiny reportResult object. If I want to attach one of each (a staged and a non-staged report), it properly attaches the non-staged report but not the staged report.

Therefore, I think it’s possible that the staging process doesn’t have time to finish before the report is being generated. Here is the pertinent function.

function ProcessGenericEmail(five, userKey, emailPayload, reports, groups, result) {

    // reports is an array of selectedReports:  [{ReportID: xxx, IsFiltered: true|false},...]
    // groups is an array of selectedGroups:    [{StudyGroupKey: xxx, StudyGroupName: xxx},...]


    let phase = 'starting generic';
    try {

        five.log(`ProcessGenericEmail: UserKey=${userKey}`);

        // verify needed information
        const hasReports = Array.isArray(reports) && reports.length > 0;
        const hasGroups = Array.isArray(groups) && groups.length > 0;

        // collect selected reports
        phase = 'generating reports';
        const attachments = [];
        for (let i = 0; i < reports.length; i++) {

            const rep = reports[i];
            const reportID = rep.ReportID;
            const runKey = five.uuid();
            const reportSettings = JSON.parse(five.getVariable('ReportSettings') || '{}');
            const mySettings = reportSettings[reportID] || {};
            
            // exit if any reports need groups and no groups are selected
            const needGroups = mySettings.NeedsGroups === true || mySettings.NeedsGroups === 'true' || mySettings.NeedsGroups === 1;
            if (needGroups && !hasGroups) {
                return five.createError(`You didn't select any groups for report: ${reportID}`);
            }

            // see if we need to stage the report before generating
            const isStaged = mySettings.Stage === true || mySettings.Stage === 'true' || mySettings.Stage === 1;
            five.log(`GenerateEmailsServer: isStaged=${isStaged}`);
            if (isStaged) {

                // stage report and verify count
                const stageTable = 'Stage' + reportID;
                five.log(`Staging report: ${reportID}`);
                StageReport(five, userKey, runKey, reportID);
                const qc = five.executeQuery(`SELECT COUNT(*) AS Cnt FROM \`${stageTable}\` WHERE UserKey = ? AND RunKey = ?`, 0, userKey, runKey);
                const rows = (qc && (qc.values || qc.rows)) || [];
                const row = rows[0] || {};
                const cnt = Number(row.Cnt ?? 0);
                five.log(`${stageTable} count for runKey=${runKey}: ${cnt}`);

            }
            
            
            // set variables and generate report
            five.setVariable('RunKey', runKey);
            five.setVariable('UserKey', userKey);
            five.log(`Vars before report: UserKey=${five.getVariable('UserKey')}, RunKey=${five.getVariable('RunKey')}`);
            
            const rr = five.executeAction(reportID, { UserKey: userKey, RunKey: runKey });
            if (rr && rr.isOk && !rr.isOk()) {
                return five.createError(`Report failed: ${reportID}`);
            }
            if (rr && rr.report) attachments.push(rr.report);

            const ok = (rr && typeof rr.isOk === 'function') ? rr.isOk() : null;
            five.log(`UserKey=${userKey}, RunKey=${runKey}, Report ${reportID}: ok=${ok}, keys=${Object.keys(rr||{}).join(',')}`);

            const r = rr && rr.report ? String(rr.report) : '';
            five.log(`UserKey=${userKey}, RunKey=${runKey}, report=${reportID}, report prefix=${r.substring(0, 40)}, len=${r.length}`);

        }
        five.log(`Attachments generated: ${attachments.length}`);

        phase = 'resolving email addresses';
        const sendTo = emailPayload.To || '';
        const ccTo = emailPayload.Cc || '';
        const bccTo = emailPayload.Bcc || '';
        const fiveSendTo = ParseEmailList(five, sendTo);
        if (!fiveSendTo || fiveSendTo.length === 0) {
            return five.createError('No valid TO email addresses were found.');
        }
        
        const fiveCcTo = ParseEmailList(five, ccTo);
        const fiveBccTo = ParseEmailList(five, bccTo);
        const subject = emailPayload.Subject || '';
        const body = emailPayload.Body || '';
        phase = 'sending email';
        const mailContext = {
            SMTPAddresses: fiveSendTo,
            SMTPCcAddresses: fiveCcTo,
            SMTPBccAddresses: fiveBccTo,
            SMTPSubject: subject,
            SMTPBody: body
        };
        five.log(`GenerateEmailsServer: ${attachments.length} attachments`);
        if (attachments.length > 0) {
        mailContext.SMTPAttachments = attachments;
        }
        
        five.log(`Sending GENERIC email to: ${sendTo}`);

        const mailResult = five.executeAction('ReportEmail', mailContext);

        if (mailResult && mailResult.isOk && !mailResult.isOk()) {
            return five.createError(mailResult);
        }

        // build success envelope
        result = five.success(result);
        result.Code = five.ErrErrorOk;
        result.Message = ''; //`Email sent (${attachments.length} attachments)`;
        result.Results = JSON.stringify({
            EmailType: 'GENERIC',
            ToCount: fiveSendTo.length,
            CcCount: fiveCcTo.length,
            BccCount: fiveBccTo.length,
            ReportCount: reports.length,
            AttachmentCount: attachments.length,
            Sent: true
        });
        five.log(`ProcessGenericEmail: result=${JSON.stringify(result)}`);
        return result;

    } catch (e) {
        five.log(`ProcessGenericEmail ERROR during "${phase}": ${e.message}`);
        return five.createError(`Error during "${phase}": ${e.message}`);
    }
}

Here is the StageReport function called from the above function:

function StageReport(five, userKey, runKey, reportID) {

    const rid = String(reportID || '').trim();
    if (!/^[A-Za-z0-9_]+$/.test(rid)) {
        throw new Error(`StageReport: invalid reportID "${rid}"`);
    }

    const stageTable = `Stage${reportID}`;
    const stageQuery = `rqStage${reportID}`;

    let phase = 'clearing prior records for this runKey';
    let sql = `DELETE FROM \`${stageTable}\` WHERE \`UserKey\` = ? AND \`RunKey\`  = ?`;
    let qr =    five.executeQuery(sql, 0, userKey, runKey);
    if (qr && qr.isOk && !qr.isOk()) {
        const err =
            (typeof qr.errorMessage === 'function' && qr.errorMessage()) ||
            qr.error || qr.message || JSON.stringify(qr);
        throw new Error(`StageReport: delete failed ${phase}: (${stageTable}): ${err}`);
    }

    // stage rows for the report
    phase = 'staging report data';
    five.log(`StageReport: phase=${phase}, qry=${stageQuery}`);

    qr = five.executeQuery(stageQuery, 0, userKey, runKey, userKey, userKey);
    if (qr && qr.isOk && !qr.isOk()) {
        const err =
            (typeof qr.errorMessage === 'function' && qr.errorMessage()) ||
            qr.error || qr.message || JSON.stringify(qr);
        throw new Error(`StageReport: stage query failed ${phase}: (${stageQuery}): ${err}`);
    }

}

Is it possible that the stage table is not done being filled yet?

for the on-demand reports, the server function is called to do the staging and in the callback function (which I believe guarantees that the server function completed), the executeAction for the report is called.

Hi Ron,

Thanks for the additional information,

When the team fixes the issue, I will also retest this scenario.

Regards,

Elton S

Hi Ron,

The team has made progress with the issue I mentioned before, but it is still not quite ready.

Meanwhile, I noticed a part of the code that needs a bit of attention.
In case you are still performing this query in the function InitialSetup, you may have a problem.

This query has no primary key and no index; therefore, when you try to submit and send an email, you will get a lock table (stage table) error message.

// clear old staging table records

for (const table of stagingTables) {

    const sql = \`

        DELETE FROM \\\`${table}\\\`

        WHERE CreatedAt < (NOW() - INTERVAL 7 DAY)

       \`;

    five.executeQuery(sql, 0);

    }

To increase the performance, you can:

Add a primary key to the query.
Add an index to the table.
Add a limit to the query.
Add a transaction to query and commit it more often.

This will not solve everything, but this will prevent the stage table to be locked.
I will continue to investigate this scenario as the team progresses on the issue, and I will keep you updated.

Regards,

Elton S

Hi Elton,

Thanks for the replies. I have some comments:

I don’t understand your last reply. InitialSetup function is run when the user logs on. Deleting old records from the various staging tables is run as part of this. but I don’t follow you on how this would cause a lock on those tables. I may not need to email reports for a long time after logging on, and not every time I log on. Can you please elaborate on why having no primary key on the query or index on the table will lock it?

More importantly, did you get a chance to review my previous reply? There I discovered that reports that weren’t “staged” seemed to work, and those that were did not. In that case I would receive an empty email. I refactored my code, and now ALL reports are staged. this means load a staging table with all the query results, each report having a UserKey and unique RunKey plus all the data fields for the report.

I used an earlier tip from you, and called a server function to stage the data, and didn’t try to run the reports until that was done, by running the second server function from within the callback. I did this because I think the staging did not get completed before the report was generated.

Now, it seems like I can successfully run and email multiple reports by using this technique. So the problem seemed to be a timing issue. Now that the staging tables are completely filled before trying to generate and attach the report, it seems to work.

Although, I still have the timeout issue, where if I run a very long report, I instead get a five error message, which is blank. But if I limit the data going into the report, it seems to work ok. So I will now start with a small project of generating those group rosters individually and putting them together afterward. Can you please request that the error message actually have the message if the timeout occurs, rather than a blank message? Whenever this “message” appears, then five logs me off automatically.

Could you please confirm exactly what the issue is on your end, that is being worked on? I’d like to know this so I can see if that is affecting me directly (since I can now run and email most reports).

Thanks so much for your support!

Hi Ron,

Yes, I did see your previous comment, and the issue I am facing on my end is likely to be related to the Stage functionality.

The function InitialSetup, which runs on the Do logon as mentioned, fetches the entire table, and teh indexes are not properly set up. Internally, when running the application, it seems that this functionality has not finished, and when the stage functionally runs, the table is locked.

In your current index, the only part of the query that is used is the ‘WHERE UserKey = ?’, the ‘or’ condition won’t work for the index.

So you may need to add a single index for this field, CreatedAt.

Additionally, it would be even better if you separate the query into two to use the ‘WHERE UserKey = ?’ and another one ‘CreatedAt < (NOW() - INTERVAL 7 DAY)’ to improve efficiency.

It also seems that the function attached to the Do logon is triggered again, which is a problem on our end (possible one of the main issues here). I am investigating it further and will let you know about its progress.

I will also suggest an optional message parameter for when the report fail for timeout.

Thank you.
Regards,
Elton S

Thanks Elton

Here is my newest InitialSetup code

function InitialSetup(five, context, result)  {

    try{
        
        const userKey = five.currentUserKey();
        five.setVariable("UserKey", userKey);
        five.clearVariable('SelectedTableInitTypes');
        GetSettings(five);
        let stagingTables = five.getVariable('StagingTables') || '[]';
        try {
            stagingTables = JSON.parse(stagingTables);
        } catch (e) {
            throw new Error(`StagingTables is not valid JSON: ${stagingTables}`);
        }
        if (!Array.isArray(stagingTables)) {
            throw new Error('StagingTables must parse to an array');
        }
        
        // clear staging tables
        let sql, qr;
        for (const table of stagingTables) {
            
            // delete all records for UserKey
            sql = `DELETE FROM \`${table}\` WHERE UserKey = ?`;
            qr = five.executeQuery(sql, 0, userKey);
            if (qr && qr.isOk && !qr.isOk()) {
                const err =
                    (typeof qr.errorMessage === 'function' && qr.errorMessage()) ||
                    qr.error || qr.message || JSON.stringify(qr);
                throw new Error(`Failed clearing ${table}: ${err}`);
            }
            
            // delete all records over 7 days old
            sql = `DELETE FROM \`${table}\` WHERE CreatedAt < (NOW() - INTERVAL 7 DAY)`;
            qr = five.executeQuery(sql, 0);
            if (qr && qr.isOk && !qr.isOk()) {
                const err =
                    (typeof qr.errorMessage === 'function' && qr.errorMessage()) ||
                    qr.error || qr.message || JSON.stringify(qr);
                throw new Error(`Failed clearing ${table}: ${err}`);
            }
        }    
        
        // save fiscal year start date
        let fyStartMo = Number(five.getVariable('FiscalYearStartMonth'));
        if (!Number.isInteger(fyStartMo) || fyStartMo < 1 || fyStartMo > 12) {
            throw new Error(`Invalid FiscalYearStartMonth: ${fyStartMo}`);
        }
        const today = new Date(five.now());
        let yr = today.getFullYear();
        let mo = today.getMonth() + 1; // month is 0-based, so add 1 before comparing to fy start month
        if (mo < fyStartMo) {
            yr -= 1;
        }
        five.setVariable("FyStartDate", new Date(yr, (fyStartMo - 1), 1));
        
        // save current date as formatted string
        const formattedDate = today.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
        five.setVariable("CurrentDate", formattedDate);
        
        five.setVariable("AddMode", false);
        
        return five.success(result);

    } catch (e) {
        return five.createError(`InitialSetup failed: ${e.message}`);
    }
}

I have separated the clearing queries as you can see. I have also added a CreatedAtIdx index to all staging tables using the field CreatedAt.

It’s not clear about adding indexes to the queries. Not sure how to do this for inline sql. Are you suggesting I create saved queries and add the index there to the queries? Is this correct for delete queries?

Thanks…

Hi Ron,

Thank you for the further details.

I managed to investigate this issue further last week, and I noticed a couple of things:

1 - I faced an issue while running the query rqStageGroupRoster, it is failing because the name of the primary key has been changed in five’s definition and not in the DB schema.
If you try to run this query, an error will be displayed. To fix it, you can navigate to the Tables menu, select the record ‘StageGroupRoster’, and click on the button ‘Create Table’, which will force the DB schema to be recreated based on Five’s definitions.

2 - The issue related to the report not getting data is because the reports in the backend are executed under another transaction/process; therefore, when you create the stage tables, they won’t be seen by the report.

I have already notified the development team about this issue, and hopefully, soon, I can provide the steps you need to apply to your code.

Regards,

Elton S

Hi Elton,

Thanks for your reply.

1: I don’t get this error, so perhaps my application/database is newer than yours. I will send you an FDF file on the other thread where you asked for it.

2: I don’t quite understand this explanation, but as I said earlier, this does not seem to happen anymore since I re-architected the system, and the staging tables are updated completely before the report is being rendered.

Hi Ron,

Thank you, I have received your latest application, could you please re-check whether this issue is still happening for you. Thank you.

Regards,
Elton S