Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions primary/scripts/dbUpdate20250806_schema_event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import type { Utilities } from 'scoutradioz-utilities';
import type { Event, Org } from 'scoutradioz-types';
import type { ObjectId } from 'mongodb';

process.env.TIER = 'dev';

const utilities: Utilities = require('scoutradioz-utilities');

utilities.config(require('../databases.json'), {
cache: {
enable: true,
maxAge: 30
},
debug: true,
});

utilities.refreshTier();

// Local types for orgschemas only to avoid dependency on changing external schema
interface OldOrgSchema {
_id?: ObjectId;
org_key: string;
year: number;
form_type: 'matchscouting' | 'pitscouting';
schema_id: ObjectId;
}

interface NewOrgSchema {
org_key: string;
event_key: string;
form_type: 'matchscouting' | 'pitscouting';
schema_id: ObjectId;
}

(async () => {
console.log('Starting migration to convert orgschemas from year-based to event-based...');

// Get all current orgschemas
const currentOrgSchemas: OldOrgSchema[] = await utilities.find('orgschemas', {});
console.log(`Found ${currentOrgSchemas.length} existing orgschemas`);

if (currentOrgSchemas.length === 0) {
console.log('No orgschemas found to migrate');
process.exit(0);
}

// Get all events to create a year -> events mapping
const allEvents: Event[] = await utilities.find('events', {});
const eventsByYear: { [year: number]: Event[] } = {};
for (const event of allEvents) {
if (!eventsByYear[event.year]) {
eventsByYear[event.year] = [];
}
eventsByYear[event.year].push(event);
}
console.log(`Found events for years: ${Object.keys(eventsByYear).join(', ')}`);

// For each orgschema, find all the events where it's being used
const newOrgSchemas: NewOrgSchema[] = [];
const schemasToRemove: OldOrgSchema[] = [];

for (const orgSchema of currentOrgSchemas) {

// Find events where this org has scouting data for this year
let eventsWithData: string[] = [];

if (orgSchema.form_type === 'matchscouting') {
// Look for match scouting data
const matchEvents = await utilities.distinct('matchscouting', 'event_key', {
org_key: orgSchema.org_key,
year: orgSchema.year
});
eventsWithData = eventsWithData.concat(matchEvents);
}
else if (orgSchema.form_type === 'pitscouting') {
// Look for pit scouting data
const pitEvents = await utilities.distinct('pitscouting', 'event_key', {
org_key: orgSchema.org_key,
year: orgSchema.year
});
eventsWithData = eventsWithData.concat(pitEvents);
}

// Remove duplicates
eventsWithData = [...new Set(eventsWithData)];

if (eventsWithData.length === 0) {
// No data found - let's check if the org has an event_key set for this year
const yearEvents = eventsByYear[orgSchema.year] || [];

// Get the org's current event_key
const org: Org | null = await utilities.findOne('orgs', { org_key: orgSchema.org_key });
const orgCurrentEventKey = org?.event_key;

if (orgCurrentEventKey) {
// Check if the org's current event is in the correct year
const orgCurrentEvent = yearEvents.find(event => event.key === orgCurrentEventKey);

if (orgCurrentEvent) {
// Create orgschema for the org's current event
const newOrgSchema: NewOrgSchema = {
org_key: orgSchema.org_key,
event_key: orgCurrentEventKey,
form_type: orgSchema.form_type,
schema_id: orgSchema.schema_id
};
newOrgSchemas.push(newOrgSchema);
}
else {
// Fallback: pick the first event in the year
if (yearEvents.length > 0) {
const fallbackEvent = yearEvents[0];
const newOrgSchema: NewOrgSchema = {
org_key: orgSchema.org_key,
event_key: fallbackEvent.key,
form_type: orgSchema.form_type,
schema_id: orgSchema.schema_id
};
newOrgSchemas.push(newOrgSchema);
}
}
}
else if (yearEvents.length > 0) {
// Org has no current event_key set, fallback to first event of the year
const fallbackEvent = yearEvents[0];
const newOrgSchema: NewOrgSchema = {
org_key: orgSchema.org_key,
event_key: fallbackEvent.key,
form_type: orgSchema.form_type,
schema_id: orgSchema.schema_id
};
newOrgSchemas.push(newOrgSchema);
}
else {
console.log(` No events found for year ${orgSchema.year}, skipping orgschema for ${orgSchema.org_key}`);
}
}
else {
// Create new orgschemas for each event where data was found
for (const event_key of eventsWithData) {
const newOrgSchema: NewOrgSchema = {
org_key: orgSchema.org_key,
event_key: event_key,
form_type: orgSchema.form_type,
schema_id: orgSchema.schema_id
};
newOrgSchemas.push(newOrgSchema);
}
}

// Mark the old orgschema for removal
schemasToRemove.push(orgSchema);
}

console.log('\nMigration plan:');
console.log(` - Remove ${schemasToRemove.length} old year-based orgschemas`);
console.log(` - Create ${newOrgSchemas.length} new event-based orgschemas`);

// Perform the migration
console.log('\nExecuting migration...');

// Remove all old orgschemas
const removeResult = await utilities.remove('orgschemas', {});
console.log(`Removed ${removeResult.deletedCount} old orgschemas`);

// Insert new event-based orgschemas
if (newOrgSchemas.length > 0) {
const insertResult = await utilities.insert('orgschemas', newOrgSchemas as any);
console.log(`Inserted ${(insertResult?.insertedCount || newOrgSchemas.length)} new orgschemas`);
}

// Verification: Check that we can still find orgschemas
const finalSchemas = await utilities.find('orgschemas', {});
console.log(`Migration complete! Final orgschema count: ${finalSchemas.length}`);

process.exit(0);
})();
2 changes: 1 addition & 1 deletion primary/src/routes/admin/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ router.get('/recalcderived', wrap(async (req, res) => {
ttokenize,
tparse,
tresolve,
} = await matchDataHelper.calculateDerivedMetrics(org_key, event_year, thisScored.data);
} = await matchDataHelper.calculateDerivedMetrics(org_key, event_key, thisScored.data);

times.db += db;
times.constructor += constructor;
Expand Down
2 changes: 1 addition & 1 deletion primary/src/routes/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ router.get('/allianceselection', wrap(async (req, res) => {
// 2020-02-11, M.O'C: Combined "scoringlayout" into "layout" with an org_key & the type "matchscouting"
let cookie_key = org_key + '_' + event_year + '_cols';
let colCookie = req.cookies[cookie_key];
let scoreLayout = await matchDataHelper.getModifiedMatchScoutingLayout(org_key, event_year, colCookie);
let scoreLayout = await matchDataHelper.getModifiedMatchScoutingLayout(org_key, event_key, colCookie);

if(!scoreLayout[0])
throw 'Couldn\'t find scoringlayout in allianceselection';
Expand Down
4 changes: 2 additions & 2 deletions primary/src/routes/manage/allianceselection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import wrap from '../../helpers/express-async-handler';
import utilities from 'scoutradioz-utilities';
import Permissions from '../../helpers/permissions';
import { matchData as matchDataHelper } from 'scoutradioz-helpers';
import e, { assert } from 'scoutradioz-http-errors';
import e from 'scoutradioz-http-errors';
import type { MongoDocument } from 'scoutradioz-utilities';
import type { Ranking, AggRange, OrgTeamValue } from 'scoutradioz-types';

Expand Down Expand Up @@ -44,7 +44,7 @@ router.get('/', wrap(async (req, res) => {
// 2020-02-11, M.O'C: Combined "scoringlayout" into "layout" with an org_key & the type "matchscouting"
let cookie_key = org_key + '_' + event_year + '_cols';
let colCookie = req.cookies[cookie_key];
let scorelayout = await matchDataHelper.getModifiedMatchScoutingLayout(org_key, event_year, colCookie);
let scorelayout = await matchDataHelper.getModifiedMatchScoutingLayout(org_key, event_key, colCookie);

let aggQuery = [];
aggQuery.push({ $match : { org_key, event_key } });
Expand Down
71 changes: 54 additions & 17 deletions primary/src/routes/manage/orgconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import express from 'express';
import { getLogger } from 'log4js';
import e, { HttpError, assert } from 'scoutradioz-http-errors';
import { upload as uploadHelper } from 'scoutradioz-helpers';
import type { Layout, MatchFormData, MatchScouting, OrgSchema, SchemaItem, Schema, SprCalculation, Upload } from 'scoutradioz-types';
import type { Layout, MatchFormData, MatchScouting, OrgSchema, Schema, SprCalculation, Upload } from 'scoutradioz-types';
import type { MongoDocument } from 'scoutradioz-utilities';
import utilities from 'scoutradioz-utilities';
import wrap from '../../helpers/express-async-handler';
Expand Down Expand Up @@ -170,14 +170,9 @@ router.get('/editform', wrap(async (req, res) => {

let org_key = req._user.org_key;

let year = parseInt(String(req.query.year)) || req.event.year;
if (!year || isNaN(year)) throw new e.UserError('Either "year" or "key" must be set.');

if (year === -1) {
let currentYear = new Date().getFullYear();
logger.debug(`Year is -1, aka, event not set. Setting year to current year: ${currentYear}`);
year = currentYear;
}
// Use the current event for form editing
let event_key = req.event.key;
if (!event_key) throw new e.UserError('No current event set.');

// load form definition data from the database
let schema: Schema | undefined,
Expand All @@ -199,25 +194,62 @@ router.get('/editform', wrap(async (req, res) => {
}`;

const orgschema = await utilities.findOne('orgschemas',
{ org_key, year, form_type },
{ org_key, event_key, form_type },
);
if (orgschema) {
schema = await utilities.findOne('schemas',
{ _id: orgschema.schema_id, owners: org_key },
);
assert(schema, `For ${org_key} and ${year}, orgschema existed in the database but pointed to nonexistent schema!`);
assert(schema, `For ${org_key} and ${event_key}, orgschema existed in the database but pointed to nonexistent schema!`);
// Create string representation of layout
layout = JSON.stringify(schema.layout).replace(/`/g, '\\`');
// 2025-02-01, M.O'C: Only do if SPR calculation exists
if (schema.spr_calculation)
sprLayout = JSON.stringify(schema.spr_calculation).replace(/`/g, '\\`');
else
logger.info(`For ${org_key} and ${year}, orgschema existed in the database but had no SPR calculation - using default`);
logger.info(`For ${org_key} and ${event_key}, orgschema existed in the database but had no SPR calculation - using default`);
}
else {
// No schema for current event - check if org has schemas from other events in the same year
logger.info(`No orgschema found for ${org_key} at ${event_key}, checking for schemas from other events in year ${req.event.year}`);

// Find all events in the same year
const eventsInYear = await utilities.find('events', { year: req.event.year });
const eventKeysInYear = eventsInYear.map(event => event.key);

// Find orgschemas for this org and form_type from any event in the same year
const orgSchemasInYear = await utilities.find('orgschemas', {
org_key,
form_type,
event_key: { $in: eventKeysInYear }
});

if (orgSchemasInYear.length > 0) {
// Get the schemas and find the most recently updated one
const schemaIds = orgSchemasInYear.map(os => os.schema_id);
const schemasInYear = await utilities.find('schemas', {
_id: { $in: schemaIds },
owners: org_key
}, { sort: { last_modified: -1 } }); // Sort by most recent first

if (schemasInYear.length > 0) {
const mostRecentSchema = schemasInYear[0];
logger.info(`Found ${schemasInYear.length} existing schemas for ${org_key} in year ${req.event.year}, using most recent from ${mostRecentSchema.last_modified}`);

// Use the most recent schema as a template
layout = JSON.stringify(mostRecentSchema.layout).replace(/`/g, '\\`');
if (mostRecentSchema.spr_calculation)
sprLayout = JSON.stringify(mostRecentSchema.spr_calculation).replace(/`/g, '\\`');

// Don't set the schema variable since we want to create a new one, just use the layout
logger.info(`Using existing schema layout as template for new event ${event_key}`);
}
}
}

// Get name, description, and whether it's published from the schema (or assign defaults)
let { name, description, published } = schema || {
name: `${org_key}'s ${year} ${form_type} Form`,
name: `${org_key}'s ${event_key} ${form_type} Form`,
description: '',
published: false
};
Expand All @@ -226,7 +258,7 @@ router.get('/editform', wrap(async (req, res) => {
let existingFormData = new Map<string, string>();
let previousDataExists = false;
// get existing data schema (if any)
let matchDataFind: MatchScouting[] = await utilities.find('matchscouting', { org_key, year, 'data': { $exists: true } }, {});
let matchDataFind: MatchScouting[] = await utilities.find('matchscouting', { org_key, event_key, 'data': { $exists: true } }, {});
matchDataFind.forEach((element) => {
let thisMatch: MatchScouting = element;
if (thisMatch['data']) {
Expand Down Expand Up @@ -267,7 +299,8 @@ router.get('/editform', wrap(async (req, res) => {
published,
form_type,
org_key,
year,
event_key,
year: req.event.year,
previousDataExists,
previousKeys
});
Expand Down Expand Up @@ -297,6 +330,10 @@ router.post('/submitform', wrap(async (req, res) => {
assert(!isNaN(year), 'invalid year!');
assert(['matchscouting', 'pitscouting'].includes(form_type), 'invalid form_type!');

// Convert year to event_key using current event
const event_key = req.event.key;
if (!event_key) throw new e.UserError('No current event set.');

// Get the list of org images (for checking image IDs in form)
const orgImages = await uploadHelper.findOrgImages(org_key, year);
const orgImageKeys = Object.keys(orgImages);
Expand Down Expand Up @@ -337,7 +374,7 @@ router.post('/submitform', wrap(async (req, res) => {

// Get existing schema metadata from db
const orgschema = await utilities.findOne('orgschemas',
{ org_key, year, form_type },
{ org_key, event_key, form_type },
);
// schema did exist in db; update it now
if (orgschema) {
Expand Down Expand Up @@ -382,7 +419,7 @@ router.post('/submitform', wrap(async (req, res) => {

let newOrgSchema: OrgSchema = {
org_key,
year,
event_key,
form_type,
schema_id: insertResult.insertedId,
};
Expand Down
Loading