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 +182 -0
- package/db/index.js +3 -0
- package/db/mongodb.js +98 -0
- package/index.js +9 -0
- package/models/base.js +121 -0
- package/models/betTracker.js +32 -0
- package/models/bookmaker.js +52 -0
- package/models/competition.js +163 -0
- package/models/fixture.js +218 -0
- package/models/index.js +30 -0
- package/models/market.js +38 -0
- package/models/marketTemplate.js +102 -0
- package/models/odd.js +78 -0
- package/models/oddDebug.js +32 -0
- package/models/player.js +140 -0
- package/models/sport.js +145 -0
- package/models/team.js +133 -0
- package/models/user.js +62 -0
- package/package.json +29 -0
- package/utils/index.js +4 -0
- package/utils/logging/config.js +41 -0
- package/utils/logging/logger.js +55 -0
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
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
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 };
|