Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to solar eclipse endpoint #104

Merged
merged 10 commits into from
Mar 8, 2024
1,564 changes: 839 additions & 725 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"start": "npm run serve"
},
"dependencies": {
"@effect/schema": "^0.63.2",
"@types/body-parser": "^1.19.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
Expand All @@ -14,6 +15,8 @@
"@types/express-session": "^1.17.4",
"@types/jsonwebtoken": "^8.5.8",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"body-parser": "^1.19.1",
"connect-session-sequelize": "^7.1.3",
"cookie-parser": "^1.4.6",
Expand All @@ -27,15 +30,13 @@
"mysql2": "^2.3.3",
"nanoid": "^3.3.2",
"path": "^0.12.7",
"sequelize": "^6.21.3",
"sequelize": "^6.37.1",
"sha3": "^2.1.4",
"typescript": "^4.6.3",
"typescript": "^5.4.2",
"uuid": "^8.3.2",
"winston": "^3.10.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^5.19.0",
"eslint": "^8.13.0",
"eslint-plugin-vue": "^8.6.0",
"sass": "~1.32.0",
Expand Down
4 changes: 2 additions & 2 deletions src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export type LoginResponse = {

export type CreateClassResponse = {
result: CreateClassResult;
class?: object;
class?: object | undefined;
}

// Grab any environment variables
Expand Down Expand Up @@ -313,7 +313,7 @@ async function checkLogin<T extends Model & User>(email: string, password: strin
}
return {
result: result,
id: user?.id,
id: user?.id ?? 0,
success: LoginResult.success(result)
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/models/dashboard_class_group.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Sequelize, DataTypes, Model, InferAttributes, InferCreationAttributes, CreationOptional, DATE } from "sequelize";
import { Sequelize, DataTypes, Model, InferAttributes, InferCreationAttributes, CreationOptional } from "sequelize";

export class DashboardClassGroup extends Model<InferAttributes<DashboardClassGroup>, InferCreationAttributes<DashboardClassGroup>> {
declare id: CreationOptional<number>;
Expand Down
80 changes: 56 additions & 24 deletions src/stories/solar-eclipse-2024/database.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,78 @@
import * as S from "@effect/schema/Schema";
import { cosmicdsDB } from "../../database";
import { logger } from "../../logger";
import {
isArrayThatSatisfies,
isNumberArray,
} from "../../utils";

import { initializeModels, SolarEclipse2024Response } from "./models";
import { UpdateAttributes } from "../../utils";
import { initializeModels, SolarEclipse2024Data } from "./models";

type SolarEclipse2024UpdateAttributes = UpdateAttributes<SolarEclipse2024Data>;

initializeModels(cosmicdsDB);

export interface SolarEclipse2024Data {
user_uuid: string;
user_selected_locations: [number, number][],
timestamp: Date
}
const LatLonArray = S.mutable(S.array(S.mutable(S.tuple(S.number, S.number))));

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isValidSolarEclipseData(data: any): data is SolarEclipse2024Response {
return typeof data.user_uuid === "string" &&
isArrayThatSatisfies(data.user_selected_locations, (arr) => {
return arr.every(x => isNumberArray(x) && x.length === 2);
});
}
export const SolarEclipse2024Entry = S.struct({
user_uuid: S.string,
user_selected_locations: LatLonArray,
cloud_cover_selected_locations: LatLonArray,
info_time_ms: S.optional(S.number.pipe(S.int()), { exact: true }),
app_time_ms: S.optional(S.number.pipe(S.int()), { exact: true }),
});

export async function submitSolarEclipse2024Response(data: SolarEclipse2024Response): Promise<SolarEclipse2024Response | null> {
export const SolarEclipse2024Update = S.struct({
user_selected_locations: S.optional(LatLonArray, { exact: true }),
cloud_cover_selected_locations: S.optional(LatLonArray, { exact: true }),
delta_info_time_ms: S.optional(S.number.pipe(S.int()), { exact: true }),
delta_app_time_ms: S.optional(S.number.pipe(S.int()), { exact: true }),
});

export type SolarEclipse2024DataT = S.Schema.To<typeof SolarEclipse2024Entry>;
export type SolarEclipse2024UpdateT = S.Schema.To<typeof SolarEclipse2024Update>;

export async function submitSolarEclipse2024Data(data: SolarEclipse2024DataT): Promise<SolarEclipse2024Data | null> {
logger.verbose(`Attempting to submit solar eclipse 2024 measurement for user ${data.user_uuid}`);

const dataWithCounts = {
...data,
user_selected_locations_count: data.user_selected_locations.length
user_selected_locations_count: data.user_selected_locations.length,
cloud_cover_selected_locations_count: data.cloud_cover_selected_locations.length,
};

return SolarEclipse2024Response.upsert(dataWithCounts).then(([item, _]) => item);
return SolarEclipse2024Data.upsert(dataWithCounts).then(([item, _]) => item);
}

export async function getAllSolarEclipse2024Responses(): Promise<SolarEclipse2024Response[]> {
return SolarEclipse2024Response.findAll();
export async function getAllSolarEclipse2024Data(): Promise<SolarEclipse2024Data[]> {
return SolarEclipse2024Data.findAll();
}

export async function getSolarEclipse2024Response(userUUID: string): Promise<SolarEclipse2024Response | null> {
return SolarEclipse2024Response.findOne({
export async function getSolarEclipse2024Data(userUUID: string): Promise<SolarEclipse2024Data | null> {
return SolarEclipse2024Data.findOne({
where: { user_uuid: userUUID }
});
}

export async function updateSolarEclipse2024Data(userUUID: string, update: SolarEclipse2024UpdateT): Promise<boolean> {
const data = await SolarEclipse2024Data.findOne({ where: { user_uuid: userUUID } });
if (data === null) {
return false;
}
const dbUpdate: SolarEclipse2024UpdateAttributes = {};
if (update.user_selected_locations) {
const selected = data.user_selected_locations.concat(update.user_selected_locations);
dbUpdate.user_selected_locations = selected;
dbUpdate.user_selected_locations_count = selected.length;
}
if (update.cloud_cover_selected_locations) {
const selected = data.cloud_cover_selected_locations.concat(update.cloud_cover_selected_locations);
dbUpdate.cloud_cover_selected_locations = selected;
dbUpdate.cloud_cover_selected_locations_count = selected.length;
}
if (update.delta_info_time_ms) {
dbUpdate.info_time_ms = data.info_time_ms + update.delta_info_time_ms;
}
if (update.delta_app_time_ms) {
dbUpdate.app_time_ms = data.app_time_ms + update.delta_app_time_ms;
}
const result = await data.update(dbUpdate);
return result !== null;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { Sequelize, DataTypes, Model, InferAttributes, InferCreationAttributes, CreationOptional } from "sequelize";

export class SolarEclipse2024Response extends Model<InferAttributes<SolarEclipse2024Response>, InferCreationAttributes<SolarEclipse2024Response>> {
export class SolarEclipse2024Data extends Model<InferAttributes<SolarEclipse2024Data>, InferCreationAttributes<SolarEclipse2024Data>> {
declare id: CreationOptional<number>;
declare user_uuid: string;
declare user_selected_locations: [number, number][];
declare user_selected_locations_count: number;
declare cloud_cover_selected_locations: [number, number][];
declare cloud_cover_selected_locations_count: number;
declare info_time_ms: CreationOptional<number>;
declare app_time_ms: CreationOptional<number>;
declare timestamp: CreationOptional<Date>;
}

export function initializeSolarEclipse2024ResponseModel(sequelize: Sequelize) {
SolarEclipse2024Response.init({
export function initializeSolarEclipse2024DataModel(sequelize: Sequelize) {
SolarEclipse2024Data.init({
id: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
Expand All @@ -29,13 +33,31 @@ export function initializeSolarEclipse2024ResponseModel(sequelize: Sequelize) {
type: DataTypes.INTEGER,
allowNull: false
},
cloud_cover_selected_locations: {
type: DataTypes.JSON,
allowNull: false
},
cloud_cover_selected_locations_count: {
type: DataTypes.INTEGER,
allowNull: false
},
info_time_ms: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
app_time_ms: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
timestamp: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: Sequelize.literal("CURRENT_TIMESTAMP")
}
}, {
sequelize,
engine: "InnoDB"
engine: "InnoDB",
});
}
6 changes: 3 additions & 3 deletions src/stories/solar-eclipse-2024/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Sequelize } from "sequelize";
import { SolarEclipse2024Response, initializeSolarEclipse2024ResponseModel } from "./eclipse_response";
import { SolarEclipse2024Data, initializeSolarEclipse2024DataModel } from "./eclipse_data";

export {
SolarEclipse2024Response
SolarEclipse2024Data
};

export function initializeModels(db: Sequelize) {
initializeSolarEclipse2024ResponseModel(db);
initializeSolarEclipse2024DataModel(db);
}
58 changes: 46 additions & 12 deletions src/stories/solar-eclipse-2024/router.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,71 @@
import * as S from "@effect/schema/Schema";
import * as Either from "effect/Either";
import { Router } from "express";
import { getAllSolarEclipse2024Responses, getSolarEclipse2024Response, isValidSolarEclipseData, submitSolarEclipse2024Response } from "./database";
import {
getAllSolarEclipse2024Data,
getSolarEclipse2024Data,
submitSolarEclipse2024Data,
SolarEclipse2024Entry,
updateSolarEclipse2024Data,
SolarEclipse2024Update,
} from "./database";

const router = Router();

router.put("/response", async (req, res) => {
router.put("/data", async (req, res) => {
const data = req.body;
const valid = isValidSolarEclipseData(data);
const maybe = S.decodeUnknownEither(SolarEclipse2024Entry)(data);

if (!valid) {
if (Either.isLeft(maybe)) {
res.status(400);
res.json({ error: "Malformed response submission" });
res.json({ error: "Malformed data submission" });
return;
}

const response = await submitSolarEclipse2024Response(data);
const response = await submitSolarEclipse2024Data(maybe.right);
if (response === null) {
res.status(400);
res.json({ error: "Error creating solar eclipse 2024 response" });
res.json({ error: "Error creating solar eclipse 2024 entry" });
return;
}

res.json({ response });
});

router.get("/responses", async (_req, res) => {
const responses = await getAllSolarEclipse2024Responses();
router.get("/data", async (_req, res) => {
const responses = await getAllSolarEclipse2024Data();
res.json({ responses });
});

router.get("/response/:userUUID", async (req, res) => {
const uuid = req.params.userUUID as string;
const response = await getSolarEclipse2024Response(uuid);
router.get("/data/:uuid", async (req, res) => {
const uuid = req.params.uuid as string;
const response = await getSolarEclipse2024Data(uuid);
res.json({ response });
});

router.patch("/data/:uuid", async (req, res) => {
const uuid = req.params.uuid as string;
const data = req.body;

const maybe = S.decodeUnknownEither(SolarEclipse2024Update)(data);
if (Either.isLeft(maybe)) {
res.status(400).json({ error: "Malformed update submission" });
return;
}

const response = await getSolarEclipse2024Data(uuid);
if (response === null) {
res.status(404).json({ error: "Specified user data does not exist" });
return;
}

const success = await updateSolarEclipse2024Data(uuid, maybe.right);
if (!success) {
res.status(500).json({ error: "Error updating user data" });
return;
}
res.json({ response });

});

export default router;

This file was deleted.

5 changes: 5 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { nanoid } from "nanoid";
import { enc, SHA256 } from "crypto-js";
import { v5 } from "uuid";

import { Model } from "sequelize";

// This type describes objects that we're allowed to pass to a model's `update` method
export type UpdateAttributes<M extends Model> = Parameters<M["update"]>[0];

export function createVerificationCode(): string {
return nanoid(21);
}
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
"exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
Expand Down
Loading