crazy-odds-bet-shared 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # Shared
2
+
3
+ Shared MongoDB models and database utilities for the odds project.
4
+
5
+ ## Features
6
+
7
+ - **MongoDB Connection Manager**: Easy connect/disconnect functionality
8
+ - **Mongoose Models**: Pre-built models with TypeScript support
9
+ - Bookmakers
10
+ - Sports
11
+ - Competitions
12
+ - Teams
13
+ - Fixtures
14
+ - **Common Schema Utilities**: UUID-based IDs, timestamps, external references
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ cd shared
20
+ npm install
21
+ npm run build
22
+ ```
23
+
24
+ ## Configuration
25
+
26
+ Copy `.env.example` to `.env` and configure your MongoDB connection. The shared
27
+ package loads its own `.env` file and overrides other envs, so the API and
28
+ collector both use these settings.
29
+
30
+ ```bash
31
+ cp .env.example .env
32
+ ```
33
+
34
+ Edit `.env` (Atlas SRV example):
35
+
36
+ ```env
37
+ MONGODB_URI=mongodb+srv://username:password@cluster0.gmu25o7.mongodb.net/?retryWrites=true&w=majority
38
+ DB_NAME=odds_dev
39
+ ```
40
+
41
+ Local MongoDB example:
42
+
43
+ ```env
44
+ MONGODB_URI=mongodb://127.0.0.1:27017
45
+ DB_NAME=odds_project
46
+ ```
47
+
48
+ DNS note (Windows): If SRV resolution fails, verify your cluster host resolves:
49
+
50
+ ```powershell
51
+ nslookup _mongodb._tcp.<your-cluster>.mongodb.net
52
+ nslookup <your-cluster>.mongodb.net
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ### Connecting to MongoDB
58
+
59
+ ```typescript
60
+ import { MongoDB } from "@odds-project/shared/db";
61
+
62
+ // Connect using environment variables (from shared/.env)
63
+ await MongoDB.connect();
64
+
65
+ // Or provide connection details explicitly
66
+ await MongoDB.connect("mongodb://localhost:27017", "odds_db");
67
+
68
+ // Check connection
69
+ if (MongoDB.isConnected()) {
70
+ console.log("Connected!");
71
+ }
72
+
73
+ // Disconnect
74
+ await MongoDB.disconnect();
75
+ ```
76
+
77
+ ### Using Models
78
+
79
+ ```typescript
80
+ import {
81
+ Bookmaker,
82
+ Fixture,
83
+ FixtureStatus,
84
+ Sport,
85
+ } from "@odds-project/shared/models";
86
+
87
+ // Create a bookmaker
88
+ const bookmaker = await Bookmaker.create({
89
+ name: "Bet365",
90
+ slug: "bet365",
91
+ country: "UK",
92
+ website: "https://www.bet365.com",
93
+ isActive: true,
94
+ metadata: { rating: 4.5 },
95
+ });
96
+
97
+ // Create a sport
98
+ const sport = await Sport.create({
99
+ name: "Football",
100
+ slug: "football",
101
+ displayName: "Football",
102
+ isActive: true,
103
+ externalRefs: [
104
+ {
105
+ id: "ext-123",
106
+ name: "Provider A",
107
+ slug: "provider-a",
108
+ isActive: true,
109
+ metadata: {},
110
+ },
111
+ ],
112
+ });
113
+
114
+ // Create a fixture
115
+ const fixture = await Fixture.create({
116
+ sportId: sport._id,
117
+ competitionId: "comp-uuid",
118
+ homeTeamId: "team1-uuid",
119
+ awayTeamId: "team2-uuid",
120
+ scheduledAt: new Date("2025-12-25T15:00:00Z"),
121
+ status: FixtureStatus.SCHEDULED,
122
+ externalRefs: [],
123
+ metadata: { venue: "Stadium Name" },
124
+ });
125
+
126
+ // Query fixtures
127
+ const upcomingFixtures = await Fixture.find({
128
+ status: FixtureStatus.SCHEDULED,
129
+ scheduledAt: { $gte: new Date() },
130
+ }).sort({ scheduledAt: 1 });
131
+ ```
132
+
133
+ ## Models
134
+
135
+ ### Bookmaker
136
+
137
+ - name, slug, country, website
138
+ - isActive flag
139
+ - metadata object
140
+
141
+ ### Sport
142
+
143
+ - name, slug, displayName
144
+ - isActive flag
145
+ - externalRefs array
146
+
147
+ ### Competition
148
+
149
+ - sportId (ref to Sport)
150
+ - name, slug, country, season
151
+ - isActive flag
152
+ - externalRefs array
153
+
154
+ ### Team
155
+
156
+ - sportId (ref to Sport)
157
+ - name, slug
158
+ - isActive flag
159
+ - externalRefs array
160
+
161
+ ### Fixture
162
+
163
+ - sportId, competitionId, homeTeamId, awayTeamId (refs)
164
+ - scheduledAt (Date)
165
+ - status (scheduled | live | finished | postponed | cancelled)
166
+ - externalRefs array
167
+ - metadata object
168
+
169
+ ## Common Features
170
+
171
+ All models include:
172
+
173
+ - `_id`: UUID string (generated automatically)
174
+ - `createdAt`: timestamp
175
+ - `updatedAt`: timestamp
176
+ - No version key (`__v`)
177
+
178
+ ## Scripts
179
+
180
+ - `npm run build`: Compile TypeScript to JavaScript
181
+ - `npm run dev`: Watch mode for development
182
+ - `npm run clean`: Remove dist folder
package/db/index.js ADDED
@@ -0,0 +1,3 @@
1
+ const { MongoDB } = require('./mongodb');
2
+
3
+ module.exports = { MongoDB };
package/db/mongodb.js ADDED
@@ -0,0 +1,98 @@
1
+ const mongoose = require('mongoose');
2
+ const dotenv = require('dotenv');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { logger } = require('../utils/logging/logger');
6
+
7
+ // Load environment variables from shared/.env as a fallback without overriding existing env
8
+ const sharedEnvPath = path.resolve(__dirname, '../../.env');
9
+ if (fs.existsSync(sharedEnvPath)) {
10
+ dotenv.config({ path: sharedEnvPath, override: false });
11
+ }
12
+
13
+ // Suppress duplicate schema index warnings - these are benign
14
+ const originalEmitWarning = process.emitWarning;
15
+ process.emitWarning = function (warning, ...args) {
16
+ if (typeof warning === 'string' && warning.includes('Duplicate schema index')) {
17
+ return;
18
+ }
19
+ if (warning?.message?.includes?.('Duplicate schema index')) {
20
+ return;
21
+ }
22
+ return originalEmitWarning.apply(process, [warning, ...args]);
23
+ };
24
+
25
+ /**
26
+ * MongoDB connection manager for collector and api apps
27
+ */
28
+ class MongoDB {
29
+ static connection = null;
30
+
31
+ /**
32
+ * Connect to MongoDB using environment variables or provided connection string
33
+ * @param connectionString - Optional MongoDB connection URI (overrides env)
34
+ * @param dbName - Optional database name (overrides env)
35
+ * @param options - Additional mongoose connection options
36
+ */
37
+ static async connect(
38
+ connectionString,
39
+ dbName,
40
+ options = {}
41
+ ) {
42
+ try {
43
+ const uri = connectionString || process.env.MONGODB_URI;
44
+ const database = dbName || process.env.DB_NAME;
45
+
46
+ if (!uri) {
47
+ throw new Error('MongoDB URI not provided. Set MONGODB_URI in .env or pass as parameter.');
48
+ }
49
+
50
+ // Construct full connection string with database name
51
+ let fullUri = uri;
52
+ if (database) {
53
+ // If URI doesn't already specify a database, append it
54
+ if (!uri.includes('mongodb.net/') || uri.endsWith('mongodb.net/')) {
55
+ fullUri = uri.replace(/\/$/, '') + '/' + database;
56
+ } else if (uri.includes('?')) {
57
+ fullUri = uri.replace('?', database + '?');
58
+ }
59
+ }
60
+
61
+ this.connection = await mongoose.connect(fullUri, options);
62
+ logger.info(`✓ Connected to MongoDB${database ? ` (database: ${database})` : ''}`);
63
+ } catch (error) {
64
+ logger.error('✗ Failed to connect to MongoDB:', error);
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Disconnect from MongoDB
71
+ */
72
+ static async disconnect() {
73
+ if (this.connection) {
74
+ await mongoose.disconnect();
75
+ this.connection = null;
76
+ logger.info('✓ Disconnected from MongoDB');
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get the mongoose connection instance
82
+ */
83
+ static getConnection() {
84
+ if (!this.connection) {
85
+ throw new Error('Database not initialized. Call connect() first.');
86
+ }
87
+ return this.connection;
88
+ }
89
+
90
+ /**
91
+ * Check if connected to MongoDB
92
+ */
93
+ static isConnected() {
94
+ return mongoose.connection.readyState === 1;
95
+ }
96
+ }
97
+
98
+ module.exports = { MongoDB };
package/index.js ADDED
@@ -0,0 +1,9 @@
1
+ const db = require('./db');
2
+ const models = require('./models');
3
+ const utils = require('./utils');
4
+
5
+ module.exports = {
6
+ ...db,
7
+ ...models,
8
+ ...utils
9
+ };
package/models/base.js ADDED
@@ -0,0 +1,121 @@
1
+ const { Schema } = require('mongoose');
2
+ const { v4: uuidv4 } = require('uuid');
3
+
4
+ /**
5
+ * External reference schema
6
+ */
7
+ const externalRefSchema = new Schema(
8
+ {
9
+ id: { type: String, required: true },
10
+ name: { type: String, required: true },
11
+ slug: { type: String, required: false, unique: false, sparse: true },
12
+ isActive: { type: Boolean, default: true },
13
+ metadata: { type: Schema.Types.Mixed, default: {} }
14
+ },
15
+ { _id: false }
16
+ );
17
+
18
+ /**
19
+ * Base schema options for all models
20
+ */
21
+ const baseSchemaOptions = {
22
+ timestamps: true,
23
+ versionKey: false,
24
+ _id: false,
25
+ minimize: false // Keep empty objects (e.g., specifiers) persisted across all schemas
26
+ };
27
+
28
+ /**
29
+ * Creates base schema fields with custom _id using uuidv4
30
+ */
31
+ const createBaseSchema = () => ({
32
+ _id: {
33
+ type: String,
34
+ default: () => uuidv4(),
35
+ alias: 'id'
36
+ }
37
+ });
38
+
39
+ /**
40
+ * Generates a slug from a string
41
+ */
42
+ const generateSlug = (text) => {
43
+ return text
44
+ .toLowerCase()
45
+ .trim()
46
+ .replace(/\s+/g, '-')
47
+ .replace(/[^\w-]/g, '');
48
+ };
49
+
50
+ /**
51
+ * Adds auto-slug generation pre-save hook to a schema
52
+ * @param schema The mongoose schema to add the hook to
53
+ * @param sourceField The field to generate the slug from (defaults to 'name')
54
+ */
55
+ const addAutoSlugHook = (schema, sourceField = 'name') => {
56
+ schema.pre('save', function (next) {
57
+ if (this.isModified(sourceField) || !this.get('slug')) {
58
+ const sourceValue = this.get(sourceField);
59
+ if (sourceValue) {
60
+ this.set('slug', generateSlug(sourceValue));
61
+ }
62
+ }
63
+ next();
64
+ });
65
+ };
66
+
67
+ /**
68
+ * Adds slug generation for external reference arrays on save
69
+ * @param schema The mongoose schema to add the hook to
70
+ * @param refPath The path to the external refs array (defaults to 'externalRefs')
71
+ * @param sourceField The field to generate the slug from inside each ref (defaults to 'name')
72
+ */
73
+ const addExternalRefsSlugHook = (
74
+ schema,
75
+ refPath = 'externalRefs',
76
+ sourceField = 'name'
77
+ ) => {
78
+ schema.pre('save', function (next) {
79
+ const refs = this.get(refPath);
80
+ if (Array.isArray(refs)) {
81
+ const updated = refs.map((ref) => {
82
+ if (ref && !ref.slug && ref[sourceField]) {
83
+ return { ...ref, slug: generateSlug(ref[sourceField]) };
84
+ }
85
+ return ref;
86
+ });
87
+ this.set(refPath, updated);
88
+ } else if (refs && typeof refs === 'object') {
89
+ if (refs instanceof Map) {
90
+ const updated = new Map(refs);
91
+ updated.forEach((ref, key) => {
92
+ if (ref && !ref.slug && ref[sourceField]) {
93
+ updated.set(key, { ...ref, slug: generateSlug(ref[sourceField]) });
94
+ }
95
+ });
96
+ this.set(refPath, updated);
97
+ } else {
98
+ const updated = {};
99
+ for (const key of Object.keys(refs)) {
100
+ const ref = refs[key];
101
+ if (ref && !ref.slug && ref[sourceField]) {
102
+ updated[key] = { ...ref, slug: generateSlug(ref[sourceField]) };
103
+ } else {
104
+ updated[key] = ref;
105
+ }
106
+ }
107
+ this.set(refPath, updated);
108
+ }
109
+ }
110
+ next();
111
+ });
112
+ };
113
+
114
+ module.exports = {
115
+ externalRefSchema,
116
+ baseSchemaOptions,
117
+ createBaseSchema,
118
+ generateSlug,
119
+ addAutoSlugHook,
120
+ addExternalRefsSlugHook
121
+ };
@@ -0,0 +1,32 @@
1
+ const { Schema, model } = require('mongoose');
2
+ const { createBaseSchema, baseSchemaOptions } = require('./base');
3
+
4
+ const betTrackerSchema = new Schema(
5
+ {
6
+ ...createBaseSchema(),
7
+ groupId: { type: String },
8
+ bookmakerId: { type: String, required: true, ref: 'Bookmaker' },
9
+ bookmakerName: { type: String },
10
+ fixtureId: { type: String, required: true, ref: 'Fixture' },
11
+ fixtureName: { type: String },
12
+ fixtureDate: { type: Date },
13
+ sportName: { type: String },
14
+ competitionName: { type: String },
15
+ marketName: { type: String },
16
+ odd: { type: Number, required: true },
17
+ oddName: { type: String },
18
+ oddStatus: { type: String, default: 'pending' },
19
+ stake: { type: Number, required: true },
20
+ type: { type: String, default: 'arbitrage' },
21
+ notes: { type: String }
22
+ },
23
+ baseSchemaOptions
24
+ );
25
+
26
+ betTrackerSchema.index({ fixtureId: 1 });
27
+ betTrackerSchema.index({ groupId: 1 });
28
+ betTrackerSchema.index({ bookmakerId: 1 });
29
+
30
+ const BetTracker = model('BetTracker', betTrackerSchema);
31
+
32
+ module.exports = { BetTracker };
@@ -0,0 +1,52 @@
1
+ const { Schema, model } = require('mongoose');
2
+ const { createBaseSchema, baseSchemaOptions, addAutoSlugHook } = require('./base');
3
+
4
+ const bookmakerSchema = new Schema(
5
+ {
6
+ ...createBaseSchema(),
7
+ name: { type: String, required: true },
8
+ slug: { type: String, required: false, unique: true, sparse: true },
9
+ country: { type: String, required: false, sparse: true },
10
+ website: { type: String, required: false, sparse: true },
11
+ isActive: { type: Boolean, default: true },
12
+ metadata: { type: Schema.Types.Mixed, default: {} }
13
+ },
14
+ baseSchemaOptions
15
+ );
16
+
17
+ // Indexes
18
+ bookmakerSchema.index({ isActive: 1 });
19
+ bookmakerSchema.index({ name: 'text' });
20
+
21
+ // Add auto-slug generation from name field
22
+ addAutoSlugHook(bookmakerSchema, 'name');
23
+
24
+ // Static methods
25
+ bookmakerSchema.static('findBySlug', function (slug) {
26
+ return this.findOne({ slug });
27
+ });
28
+
29
+ bookmakerSchema.static('getActive', function () {
30
+ return this.find({ isActive: true }).sort({ name: 1 });
31
+ });
32
+
33
+ bookmakerSchema.static('searchByName', function (query) {
34
+ return this.find({
35
+ name: { $regex: query, $options: 'i' }
36
+ }).sort({ name: 1 });
37
+ });
38
+
39
+ // Instance methods
40
+ bookmakerSchema.method('activate', async function () {
41
+ this.isActive = true;
42
+ return this.save();
43
+ });
44
+
45
+ bookmakerSchema.method('deactivate', async function () {
46
+ this.isActive = false;
47
+ return this.save();
48
+ });
49
+
50
+ const Bookmaker = model('Bookmaker', bookmakerSchema);
51
+
52
+ module.exports = { Bookmaker };
@@ -0,0 +1,163 @@
1
+ const { Schema, model } = require('mongoose');
2
+ const {
3
+ createBaseSchema,
4
+ baseSchemaOptions,
5
+ addAutoSlugHook,
6
+ addExternalRefsSlugHook
7
+ } = require('./base');
8
+
9
+ /**
10
+ * Competition schema
11
+ */
12
+ const competitionSchema = new Schema(
13
+ {
14
+ ...createBaseSchema(),
15
+ sportId: { type: String, required: true, ref: 'Sport' },
16
+ name: { type: String, required: true },
17
+ slug: { type: String, required: true },
18
+ country: { type: String, required: false, sparse: true },
19
+ season: { type: String, required: false, sparse: true },
20
+ isActive: { type: Boolean, default: true },
21
+ externalRefs: { type: Schema.Types.Mixed, default: {} }
22
+ },
23
+ {
24
+ ...baseSchemaOptions,
25
+ autoIndex: process.env.NODE_ENV !== 'production'
26
+ }
27
+ );
28
+
29
+ // Indexes
30
+ competitionSchema.index({ slug: 1, season: 1 }, { unique: true, sparse: true });
31
+ competitionSchema.index({ sportId: 1 });
32
+ competitionSchema.index({ isActive: 1 });
33
+ competitionSchema.index({ name: 'text' });
34
+
35
+ // Clear any duplicate indexes
36
+ if (competitionSchema.clearIndexes) {
37
+ competitionSchema.clearIndexes();
38
+ }
39
+
40
+ // Virtuals
41
+ competitionSchema.virtual('sport', {
42
+ ref: 'Sport',
43
+ localField: 'sportId',
44
+ foreignField: '_id',
45
+ justOne: true
46
+ });
47
+
48
+ // Migration hook: convert array externalRefs to object
49
+ competitionSchema.pre('save', function (next) {
50
+ if (this.externalRefs && Array.isArray(this.externalRefs)) {
51
+ // Migrate from array to object format
52
+ const converted = {};
53
+ this.externalRefs.forEach((ref) => {
54
+ if (ref && ref.bookmaker) {
55
+ // If array has bookmaker property, use it as key
56
+ const { bookmaker, ...refData } = ref;
57
+ converted[bookmaker] = refData;
58
+ } else if (ref && ref.slug) {
59
+ // Fallback to slug as key
60
+ converted[ref.slug] = ref;
61
+ }
62
+ });
63
+ this.externalRefs = converted;
64
+ }
65
+ next();
66
+ });
67
+
68
+ // Add auto-slug generation from name field
69
+ addAutoSlugHook(competitionSchema, 'name');
70
+
71
+ // Add auto-slug generation for external refs
72
+ addExternalRefsSlugHook(competitionSchema, 'externalRefs');
73
+
74
+ // Static methods
75
+ competitionSchema.static('findBySlug', function (slug, sport, season) {
76
+ const query = { slug };
77
+ if (sport) query.sportId = sport;
78
+ if (season) query.season = season;
79
+ return this.findOne(query);
80
+ });
81
+
82
+ competitionSchema.static('getActive', function () {
83
+ return this.find({ isActive: true }).sort({ name: 1 });
84
+ });
85
+
86
+ competitionSchema.static('findByExternalRef', function (bookmakerSlug, sportId, externalId) {
87
+ const query = {};
88
+ query[`externalRefs.${bookmakerSlug}.id`] = externalId;
89
+ query['sportId'] = sportId;
90
+ return this.findOne(query);
91
+ });
92
+
93
+ competitionSchema.static('findBySport', function (sportId) {
94
+ return this.find({ sportId, isActive: true }).sort({ name: 1 });
95
+ });
96
+
97
+ competitionSchema.static('searchByName', function (query) {
98
+ return this.find({
99
+ name: { $regex: query, $options: 'i' }
100
+ }).sort({ name: 1 });
101
+ });
102
+
103
+ // Instance methods
104
+ competitionSchema.method('activate', async function () {
105
+ this.isActive = true;
106
+ return this.save();
107
+ });
108
+
109
+ competitionSchema.method('deactivate', async function () {
110
+ this.isActive = false;
111
+ return this.save();
112
+ });
113
+
114
+ competitionSchema.method('addExternalRef', async function (key, ref) {
115
+ if (!this.externalRefs || typeof this.externalRefs !== 'object') {
116
+ this.externalRefs = {};
117
+ }
118
+ const refs = this.externalRefs;
119
+ refs[key] = ref;
120
+ this.markModified('externalRefs');
121
+ return this.save();
122
+ });
123
+
124
+ competitionSchema.method('removeExternalRef', async function (key) {
125
+ if (this.externalRefs && typeof this.externalRefs === 'object') {
126
+ const refs = this.externalRefs;
127
+ delete refs[key];
128
+ this.markModified('externalRefs');
129
+ }
130
+ return this.save();
131
+ });
132
+
133
+ competitionSchema.method('activateExternalRef', async function (key) {
134
+ if (this.externalRefs && typeof this.externalRefs === 'object') {
135
+ const refs = this.externalRefs;
136
+ const ref = refs[key];
137
+ if (ref) {
138
+ ref.isActive = true;
139
+ refs[key] = ref;
140
+ this.markModified('externalRefs');
141
+ return this.save();
142
+ }
143
+ }
144
+ return this;
145
+ });
146
+
147
+ competitionSchema.method('deactivateExternalRef', async function (key) {
148
+ if (this.externalRefs && typeof this.externalRefs === 'object') {
149
+ const refs = this.externalRefs;
150
+ const ref = refs[key];
151
+ if (ref) {
152
+ ref.isActive = false;
153
+ refs[key] = ref;
154
+ this.markModified('externalRefs');
155
+ return this.save();
156
+ }
157
+ }
158
+ return this;
159
+ });
160
+
161
+ const Competition = model('Competition', competitionSchema);
162
+
163
+ module.exports = { Competition };