Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .env_example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
MBUS_API_KEY=YOUR_API_KEY
RIDE_API_KEY=YOUR_API_KEY
GOOGLE_APPLICATION_CREDENTIALS=secrets/YOUR_KEY.json
MONGODB_URI=mongodb://localhost:27017/mbus

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@
"express": "^4.19.2",
"fast-json-stable-stringify": "^2.1.0",
"fast-xml-parser": "^5.3.2",
"firebase-admin": "^13.6.0",
"firebase-admin": "^10.3.0",
"lru-cache": "^11.2.5",
"mongoose": "^9.3.3",
"ts-array-utils": "^0.5.0",
"tsx": "^4.11.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/mongoose": "^5.11.96",
"@types/node": "^20.12.12",
"@vitest/coverage-v8": "^1.2.2",
"@vitest/coverage-v8": "^4.1.2",
"typedoc": "^0.28.15",
"vitest": "^1.2.2"
"vitest": "^4.1.2"
}
}
13 changes: 8 additions & 5 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import express from "express";

import mbus from "./routes/api"
import { connectToDatabase } from "./db/connection.js";
import mbus from "./routes/api.js";
import users from "./routes/users.js";

const app = express();

app.use(express.json());
app.use("/mbus/api/v3", mbus);
app.use("/mbus/api/v3/account", users);
app.use("/docs", express.static("docs"));

const PORT = process.env.PORT || 3000;


app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
connectToDatabase().then(() => {
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
});
8 changes: 8 additions & 0 deletions src/db/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import mongoose from "mongoose";

export async function connectToDatabase(): Promise<void> {
const uri = process.env.MONGODB_URI;
if (!uri) throw new Error("MONGODB_URI is not set");
await mongoose.connect(uri);
console.log("Connected to MongoDB");
}
27 changes: 27 additions & 0 deletions src/db/models/CommutePattern.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import mongoose, { Schema, Document } from "mongoose";

export interface ICommutePattern extends Document {
accountId: mongoose.Types.ObjectId;
fromStopId: string;
toStopId: string;
routeId: string;
daysOfWeek: number[]; // 0=Sun … 6=Sat
typicalHour: number;
confidence: number;
lastSeen: Date;
}

const CommutePatternSchema = new Schema<ICommutePattern>({
accountId: { type: Schema.Types.ObjectId, required: true, ref: "InternalAccount" },
fromStopId: { type: String, required: true },
toStopId: { type: String, required: true },
routeId: { type: String, required: true },
daysOfWeek: { type: [Number], default: [] },
typicalHour: { type: Number, required: true },
confidence: { type: Number, default: 0 },
lastSeen: { type: Date, default: Date.now },
});

export const CommutePattern = mongoose.model<ICommutePattern>(
"CommutePattern", CommutePatternSchema
);
21 changes: 21 additions & 0 deletions src/db/models/InternalAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import mongoose, { Schema, Document } from "mongoose";

export interface IInternalAccount extends Document {
uniqname: string;
roles: string[];
createdAt: Date;
lastLoginAt: Date;
active: boolean;
}

const InternalAccountSchema = new Schema<IInternalAccount>({
uniqname: { type: String, required: true, unique: true },
roles: { type: [String], default: [] },
createdAt: { type: Date, default: Date.now },
lastLoginAt: { type: Date, default: Date.now },
active: { type: Boolean, default: true },
});

export const InternalAccount = mongoose.model<IInternalAccount>(
"InternalAccount", InternalAccountSchema
);
25 changes: 25 additions & 0 deletions src/db/models/NotificationLog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import mongoose, { Schema, Document } from "mongoose";

export interface INotificationLog extends Document {
accountId: mongoose.Types.ObjectId;
type: string;
routeId: string;
stopId: string;
sentAt: Date;
opened: boolean;
openedAt: Date | null;
}

const NotificationLogSchema = new Schema<INotificationLog>({
accountId: { type: Schema.Types.ObjectId, required: true, ref: "InternalAccount" },
type: { type: String, required: true },
routeId: { type: String, default: "" },
stopId: { type: String, default: "" },
sentAt: { type: Date, default: Date.now },
opened: { type: Boolean, default: false },
openedAt: { type: Date, default: null },
});

export const NotificationLog = mongoose.model<INotificationLog>(
"NotificationLog", NotificationLogSchema
);
23 changes: 23 additions & 0 deletions src/db/models/PersonalizationProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import mongoose, { Schema, Document } from "mongoose";

export interface IPersonalizationProfile extends Document {
accountId: mongoose.Types.ObjectId;
topStopIds: string[];
topRouteIds: string[];
topBuildingIds: string[];
peakUsageHours: number[];
computedAt: Date;
}

const PersonalizationProfileSchema = new Schema<IPersonalizationProfile>({
accountId: { type: Schema.Types.ObjectId, required: true, unique: true, ref: "InternalAccount" },
topStopIds: { type: [String], default: [] },
topRouteIds: { type: [String], default: [] },
topBuildingIds: { type: [String], default: [] },
peakUsageHours: { type: [Number], default: [] },
computedAt: { type: Date, default: Date.now },
});

export const PersonalizationProfile = mongoose.model<IPersonalizationProfile>(
"PersonalizationProfile", PersonalizationProfileSchema
);
23 changes: 23 additions & 0 deletions src/db/models/SavedStop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import mongoose, { Schema, Document } from "mongoose";

export interface ISavedStop extends Document {
accountId: mongoose.Types.ObjectId;
stopId: string;
customLabel: string;
pinned: boolean;
pinnedOrder: number;
savedAt: Date;
}

const SavedStopSchema = new Schema<ISavedStop>({
accountId: { type: Schema.Types.ObjectId, required: true, ref: "InternalAccount" },
stopId: { type: String, required: true },
customLabel: { type: String, default: "" },
pinned: { type: Boolean, default: false },
pinnedOrder: { type: Number, default: 0 },
savedAt: { type: Date, default: Date.now },
});
// Prevent duplicate saves for same account + stop
SavedStopSchema.index({ accountId: 1, stopId: 1 }, { unique: true });

export const SavedStop = mongoose.model<ISavedStop>("SavedStop", SavedStopSchema);
25 changes: 25 additions & 0 deletions src/db/models/SearchHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import mongoose, { Schema, Document } from "mongoose";

export interface IUserSearchHistory extends Document {
accountId: mongoose.Types.ObjectId;
query: string;
resultType: string;
resultId: string;
searchCount: number;
lastSearchedAt: Date;
}

const UserSearchHistorySchema = new Schema<IUserSearchHistory>({
accountId: { type: Schema.Types.ObjectId, required: true, ref: "InternalAccount" },
query: { type: String, required: true },
resultType: { type: String, default: "" },
resultId: { type: String, default: "" },
searchCount: { type: Number, default: 1 },
lastSearchedAt: { type: Date, default: Date.now },
});
// Upsert key: one entry per (account, query, resultId) combo
UserSearchHistorySchema.index({ accountId: 1, query: 1, resultId: 1 }, { unique: true });

export const SearchHistory = mongoose.model<IUserSearchHistory>(
"SearchHistory", UserSearchHistorySchema
);
23 changes: 23 additions & 0 deletions src/db/models/TripHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import mongoose, { Schema, Document } from "mongoose";

export interface ITripHistory extends Document {
accountId: mongoose.Types.ObjectId;
routeId: string;
boardStopId: string;
alightStopId: string;
startedAt: Date;
endedAt: Date;
source: string;
}

const TripHistorySchema = new Schema<ITripHistory>({
accountId: { type: Schema.Types.ObjectId, required: true, ref: "InternalAccount" },
routeId: { type: String, required: true },
boardStopId: { type: String, required: true },
alightStopId: { type: String, required: true },
startedAt: { type: Date, required: true },
endedAt: { type: Date, required: true },
source: { type: String, default: "manual" },
});

export const TripHistory = mongoose.model<ITripHistory>("TripHistory", TripHistorySchema);
27 changes: 27 additions & 0 deletions src/db/models/UserPreferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import mongoose, { Schema, Document } from "mongoose";

export interface IUserPreferences extends Document {
accountId: mongoose.Types.ObjectId;
defaultView: string;
notificationsEnabled: boolean;
notifyMinutesBefore: number;
preferredRouteIds: string[];
accessibilityMode: boolean;
theme: string;
updatedAt: Date;
}

const UserPreferencesSchema = new Schema<IUserPreferences>({
accountId: { type: Schema.Types.ObjectId, required: true, unique: true, ref: "InternalAccount" },
defaultView: { type: String, default: "map" },
notificationsEnabled: { type: Boolean, default: true },
notifyMinutesBefore: { type: Number, default: 5 },
preferredRouteIds: { type: [String], default: [] },
accessibilityMode: { type: Boolean, default: false },
theme: { type: String, default: "system" },
updatedAt: { type: Date, default: Date.now },
});

export const UserPreferences = mongoose.model<IUserPreferences>(
"UserPreferences", UserPreferencesSchema
);
Loading