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/models/sport.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const { Schema, model } = require('mongoose');
|
|
2
|
+
const {
|
|
3
|
+
createBaseSchema,
|
|
4
|
+
baseSchemaOptions,
|
|
5
|
+
addAutoSlugHook,
|
|
6
|
+
addExternalRefsSlugHook
|
|
7
|
+
} = require('./base');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sport schema
|
|
11
|
+
*/
|
|
12
|
+
const sportSchema = new Schema(
|
|
13
|
+
{
|
|
14
|
+
...createBaseSchema(),
|
|
15
|
+
name: { type: String, required: true },
|
|
16
|
+
slug: { type: String, required: true },
|
|
17
|
+
displayName: { type: String, required: true },
|
|
18
|
+
isActive: { type: Boolean, default: true },
|
|
19
|
+
externalRefs: { type: Schema.Types.Mixed, default: {} }
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
...baseSchemaOptions,
|
|
23
|
+
autoIndex: process.env.NODE_ENV !== 'production'
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Indexes
|
|
28
|
+
sportSchema.index({ slug: 1 }, { unique: true, sparse: true });
|
|
29
|
+
sportSchema.index({ isActive: 1 });
|
|
30
|
+
sportSchema.index({ name: 'text', displayName: 'text' });
|
|
31
|
+
|
|
32
|
+
// Clear any duplicate indexes
|
|
33
|
+
if (sportSchema.clearIndexes) {
|
|
34
|
+
sportSchema.clearIndexes();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Migration hook: convert array externalRefs to object
|
|
38
|
+
sportSchema.pre('save', function (next) {
|
|
39
|
+
if (this.externalRefs && Array.isArray(this.externalRefs)) {
|
|
40
|
+
// Migrate from array to object format
|
|
41
|
+
const converted = {};
|
|
42
|
+
this.externalRefs.forEach((ref) => {
|
|
43
|
+
if (ref && ref.bookmaker) {
|
|
44
|
+
// If array has bookmaker property, use it as key
|
|
45
|
+
const { bookmaker, ...refData } = ref;
|
|
46
|
+
converted[bookmaker] = refData;
|
|
47
|
+
} else if (ref && ref.slug) {
|
|
48
|
+
// Fallback to slug as key
|
|
49
|
+
converted[ref.slug] = ref;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
this.externalRefs = converted;
|
|
53
|
+
}
|
|
54
|
+
next();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Add auto-slug generation from name field
|
|
58
|
+
addAutoSlugHook(sportSchema, 'name');
|
|
59
|
+
|
|
60
|
+
// Add auto-slug generation for external refs
|
|
61
|
+
addExternalRefsSlugHook(sportSchema, 'externalRefs');
|
|
62
|
+
|
|
63
|
+
// Static methods
|
|
64
|
+
sportSchema.static('findBySlug', function (slug) {
|
|
65
|
+
return this.findOne({ slug });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
sportSchema.static('getActive', function () {
|
|
69
|
+
return this.find({ isActive: true }).sort({ name: 1 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
sportSchema.static('findByExternalRefId', function (refId) {
|
|
73
|
+
return this.find({ 'externalRefs.id': refId });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
sportSchema.static('searchByName', function (query) {
|
|
77
|
+
return this.find({
|
|
78
|
+
$or: [
|
|
79
|
+
{ name: { $regex: query, $options: 'i' } },
|
|
80
|
+
{ displayName: { $regex: query, $options: 'i' } }
|
|
81
|
+
]
|
|
82
|
+
}).sort({ name: 1 });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Instance methods
|
|
86
|
+
sportSchema.method('activate', async function () {
|
|
87
|
+
this.isActive = true;
|
|
88
|
+
return this.save();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
sportSchema.method('deactivate', async function () {
|
|
92
|
+
this.isActive = false;
|
|
93
|
+
return this.save();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
sportSchema.method('addExternalRef', async function (key, ref) {
|
|
97
|
+
if (!this.externalRefs || typeof this.externalRefs !== 'object') {
|
|
98
|
+
this.externalRefs = {};
|
|
99
|
+
}
|
|
100
|
+
const refs = this.externalRefs;
|
|
101
|
+
refs[key] = ref;
|
|
102
|
+
this.markModified('externalRefs');
|
|
103
|
+
return this.save();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
sportSchema.method('removeExternalRef', async function (key) {
|
|
107
|
+
if (this.externalRefs && typeof this.externalRefs === 'object') {
|
|
108
|
+
const refs = this.externalRefs;
|
|
109
|
+
delete refs[key];
|
|
110
|
+
this.markModified('externalRefs');
|
|
111
|
+
}
|
|
112
|
+
return this.save();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
sportSchema.method('activateExternalRef', async function (key) {
|
|
116
|
+
if (this.externalRefs && typeof this.externalRefs === 'object') {
|
|
117
|
+
const refs = this.externalRefs;
|
|
118
|
+
const ref = refs[key];
|
|
119
|
+
if (ref) {
|
|
120
|
+
ref.isActive = true;
|
|
121
|
+
refs[key] = ref;
|
|
122
|
+
this.markModified('externalRefs');
|
|
123
|
+
return this.save();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return this;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
sportSchema.method('deactivateExternalRef', async function (key) {
|
|
130
|
+
if (this.externalRefs && typeof this.externalRefs === 'object') {
|
|
131
|
+
const refs = this.externalRefs;
|
|
132
|
+
const ref = refs[key];
|
|
133
|
+
if (ref) {
|
|
134
|
+
ref.isActive = false;
|
|
135
|
+
refs[key] = ref;
|
|
136
|
+
this.markModified('externalRefs');
|
|
137
|
+
return this.save();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return this;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const Sport = model('Sport', sportSchema);
|
|
144
|
+
|
|
145
|
+
module.exports = { Sport };
|
package/models/team.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const { Schema, model } = require('mongoose');
|
|
2
|
+
const {
|
|
3
|
+
createBaseSchema,
|
|
4
|
+
baseSchemaOptions,
|
|
5
|
+
addAutoSlugHook,
|
|
6
|
+
addExternalRefsSlugHook
|
|
7
|
+
} = require('./base');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Team schema
|
|
11
|
+
*/
|
|
12
|
+
const teamSchema = new Schema(
|
|
13
|
+
{
|
|
14
|
+
...createBaseSchema(),
|
|
15
|
+
sportId: { type: String, required: true, ref: 'Sport' },
|
|
16
|
+
resourceId: { type: String },
|
|
17
|
+
name: { type: String, required: true },
|
|
18
|
+
slug: { type: String, unique: true, sparse: true },
|
|
19
|
+
isActive: { type: Boolean, default: true },
|
|
20
|
+
metadata: { type: Schema.Types.Mixed, default: {} },
|
|
21
|
+
externalRefs: { type: Schema.Types.Mixed, default: {} }
|
|
22
|
+
},
|
|
23
|
+
baseSchemaOptions
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Indexes
|
|
27
|
+
teamSchema.index({ slug: 1 });
|
|
28
|
+
teamSchema.index({ sportId: 1 });
|
|
29
|
+
teamSchema.index({ isActive: 1 });
|
|
30
|
+
teamSchema.index({ name: 'text' });
|
|
31
|
+
|
|
32
|
+
// Virtuals
|
|
33
|
+
teamSchema.virtual('sport', {
|
|
34
|
+
ref: 'Sport',
|
|
35
|
+
localField: 'sportId',
|
|
36
|
+
foreignField: '_id',
|
|
37
|
+
justOne: true
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Add auto-slug generation from name field
|
|
41
|
+
addAutoSlugHook(teamSchema, 'name');
|
|
42
|
+
|
|
43
|
+
// Static methods
|
|
44
|
+
teamSchema.static('findBySlug', function (slug) {
|
|
45
|
+
return this.findOne({ slug });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
teamSchema.static('getActive', function () {
|
|
49
|
+
return this.find({ isActive: true }).sort({ name: 1 });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
teamSchema.static('findByExternalRef', function (bookmakerSlug, sportId, externalId) {
|
|
53
|
+
const query = {};
|
|
54
|
+
query[`externalRefs.${bookmakerSlug}.id`] = externalId;
|
|
55
|
+
query['sportId'] = sportId;
|
|
56
|
+
return this.findOne(query);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
teamSchema.static('findByExternalRefName', function (bookmakerSlug, sportId, externalName) {
|
|
60
|
+
const query = {};
|
|
61
|
+
query[`externalRefs.${bookmakerSlug}.name`] = externalName;
|
|
62
|
+
query['sportId'] = sportId;
|
|
63
|
+
return this.findOne(query);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
teamSchema.static('findBySport', function (sportId) {
|
|
67
|
+
return this.find({ sportId, isActive: true }).sort({ name: 1 });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
teamSchema.static('searchByName', function (query) {
|
|
71
|
+
return this.find({
|
|
72
|
+
name: { $regex: query, $options: 'i' }
|
|
73
|
+
}).sort({ name: 1 });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Instance methods
|
|
77
|
+
teamSchema.method('activate', async function () {
|
|
78
|
+
this.isActive = true;
|
|
79
|
+
return this.save();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
teamSchema.method('deactivate', async function () {
|
|
83
|
+
this.isActive = false;
|
|
84
|
+
return this.save();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
teamSchema.method('addExternalRef', async function (bookmakerSlug, externalId, data) {
|
|
88
|
+
const ref = {
|
|
89
|
+
id: externalId,
|
|
90
|
+
name: data.name,
|
|
91
|
+
slug: data.slug,
|
|
92
|
+
isActive: data.isActive !== undefined ? data.isActive : true,
|
|
93
|
+
metadata: data.metadata || {}
|
|
94
|
+
};
|
|
95
|
+
this.externalRefs[bookmakerSlug] = ref;
|
|
96
|
+
this.markModified('externalRefs');
|
|
97
|
+
return this.save();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
teamSchema.method('getExternalRef', function (bookmakerSlug) {
|
|
101
|
+
return this.externalRefs?.[bookmakerSlug] || null;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
teamSchema.method('removeExternalRef', async function (bookmakerSlug) {
|
|
105
|
+
if (this.externalRefs?.[bookmakerSlug]) {
|
|
106
|
+
delete this.externalRefs[bookmakerSlug];
|
|
107
|
+
this.markModified('externalRefs');
|
|
108
|
+
return this.save();
|
|
109
|
+
}
|
|
110
|
+
return this;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
teamSchema.method('activateExternalRef', async function (bookmakerSlug) {
|
|
114
|
+
if (this.externalRefs?.[bookmakerSlug]) {
|
|
115
|
+
this.externalRefs[bookmakerSlug].isActive = true;
|
|
116
|
+
this.markModified('externalRefs');
|
|
117
|
+
return this.save();
|
|
118
|
+
}
|
|
119
|
+
return this;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
teamSchema.method('deactivateExternalRef', async function (bookmakerSlug) {
|
|
123
|
+
if (this.externalRefs?.[bookmakerSlug]) {
|
|
124
|
+
this.externalRefs[bookmakerSlug].isActive = false;
|
|
125
|
+
this.markModified('externalRefs');
|
|
126
|
+
return this.save();
|
|
127
|
+
}
|
|
128
|
+
return this;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const Team = model('Team', teamSchema);
|
|
132
|
+
|
|
133
|
+
module.exports = { Team };
|
package/models/user.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { Schema, model } = require('mongoose');
|
|
2
|
+
const { createBaseSchema, baseSchemaOptions } = require('./base');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User schema
|
|
6
|
+
*/
|
|
7
|
+
const userSchema = new Schema(
|
|
8
|
+
{
|
|
9
|
+
...createBaseSchema(),
|
|
10
|
+
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
|
|
11
|
+
password: { type: String, required: true },
|
|
12
|
+
name: { type: String, required: true },
|
|
13
|
+
role: { type: String, enum: ['admin', 'user'], default: 'user' },
|
|
14
|
+
isActive: { type: Boolean, default: true }
|
|
15
|
+
},
|
|
16
|
+
baseSchemaOptions
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// Indexes
|
|
20
|
+
userSchema.index({ isActive: 1 });
|
|
21
|
+
userSchema.index({ role: 1 });
|
|
22
|
+
|
|
23
|
+
// Static methods
|
|
24
|
+
userSchema.static('findByEmail', function (email) {
|
|
25
|
+
return this.findOne({ email: email.toLowerCase() });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
userSchema.static('getActive', function () {
|
|
29
|
+
return this.find({ isActive: true }).select('-password');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
userSchema.static('findAdmins', function () {
|
|
33
|
+
return this.find({ role: 'admin', isActive: true }).select('-password');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Instance methods
|
|
37
|
+
userSchema.method('activate', async function () {
|
|
38
|
+
this.isActive = true;
|
|
39
|
+
return this.save();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
userSchema.method('deactivate', async function () {
|
|
43
|
+
this.isActive = false;
|
|
44
|
+
return this.save();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
userSchema.method('comparePassword', async function (password) {
|
|
48
|
+
// For now, simple comparison. In production, use bcrypt
|
|
49
|
+
return this.password === password;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Middleware to exclude password from toJSON
|
|
53
|
+
userSchema.set('toJSON', {
|
|
54
|
+
transform: (doc, ret) => {
|
|
55
|
+
delete ret.password;
|
|
56
|
+
return ret;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const User = model('User', userSchema);
|
|
61
|
+
|
|
62
|
+
module.exports = { User };
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "crazy-odds-bet-shared",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared MongoDB models and utilities for odds project",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"db",
|
|
9
|
+
"models",
|
|
10
|
+
"utils"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"lint": "echo \"Lint not configured yet\"",
|
|
14
|
+
"test": "echo \"No tests yet\""
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mongodb",
|
|
18
|
+
"models",
|
|
19
|
+
"utilities",
|
|
20
|
+
"shared"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"dotenv": "^16.3.1",
|
|
26
|
+
"mongoose": "^8.0.0",
|
|
27
|
+
"uuid": "^9.0.1"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/utils/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified logging configuration
|
|
3
|
+
* Simple JSON logging for production, pretty-printed in development
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const getLoggerConfig = (nodeEnv = 'development') => {
|
|
7
|
+
const isDev = nodeEnv !== 'production';
|
|
8
|
+
const logLevel = process.env.LOG_LEVEL || (isDev ? 'debug' : 'info');
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
isDev,
|
|
12
|
+
logLevel,
|
|
13
|
+
colorize: isDev,
|
|
14
|
+
serializers: {
|
|
15
|
+
req: (req) => ({
|
|
16
|
+
method: req.method,
|
|
17
|
+
url: req.url,
|
|
18
|
+
headers: {
|
|
19
|
+
host: req.headers?.host,
|
|
20
|
+
'content-type': req.headers?.['content-type'],
|
|
21
|
+
'user-agent': req.headers?.['user-agent']
|
|
22
|
+
},
|
|
23
|
+
remoteAddress: req.ip,
|
|
24
|
+
remotePort: req.socket?.remotePort
|
|
25
|
+
}),
|
|
26
|
+
res: (res) => ({
|
|
27
|
+
statusCode: res.statusCode,
|
|
28
|
+
responseTime: res.responseTime,
|
|
29
|
+
headers: res.getHeaders?.()
|
|
30
|
+
}),
|
|
31
|
+
error: (err) => ({
|
|
32
|
+
message: err.message,
|
|
33
|
+
stack: err.stack,
|
|
34
|
+
code: err.code,
|
|
35
|
+
statusCode: err.statusCode
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
module.exports = { getLoggerConfig };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const { getLoggerConfig } = require('./config');
|
|
2
|
+
|
|
3
|
+
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
4
|
+
const config = getLoggerConfig(nodeEnv);
|
|
5
|
+
|
|
6
|
+
// Simple logger without external dependencies
|
|
7
|
+
const logger = {
|
|
8
|
+
debug: (message, data) => logMessage('DEBUG', message, data),
|
|
9
|
+
info: (message, data) => logMessage('INFO', message, data),
|
|
10
|
+
warn: (message, data) => logMessage('WARN', message, data),
|
|
11
|
+
error: (message, data) => logMessage('ERROR', message, data),
|
|
12
|
+
fatal: (message, data) => logMessage('FATAL', message, data)
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function logMessage(level, message, data) {
|
|
16
|
+
// Check if log level should be displayed
|
|
17
|
+
const logLevels = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 };
|
|
18
|
+
const currentLevel = logLevels[config.logLevel] || 1;
|
|
19
|
+
const messageLevel = logLevels[level.toLowerCase()] || 0;
|
|
20
|
+
|
|
21
|
+
if (messageLevel < currentLevel) return;
|
|
22
|
+
|
|
23
|
+
const timestamp = new Date().toISOString();
|
|
24
|
+
const logEntry = {
|
|
25
|
+
timestamp,
|
|
26
|
+
level,
|
|
27
|
+
message,
|
|
28
|
+
...(data && typeof data === 'object' ? data : {})
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (config.isDev) {
|
|
32
|
+
// Pretty print for development
|
|
33
|
+
const color = getColorCode(level);
|
|
34
|
+
console.log(
|
|
35
|
+
`${color}[${timestamp}] ${level}${'\x1b[0m'} ${message}`,
|
|
36
|
+
data && Object.keys(data).length > 0 ? JSON.stringify(data, null, 2) : ''
|
|
37
|
+
);
|
|
38
|
+
} else {
|
|
39
|
+
// JSON output for production
|
|
40
|
+
console.log(JSON.stringify(logEntry));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getColorCode(level) {
|
|
45
|
+
const colors = {
|
|
46
|
+
DEBUG: '\x1b[36m', // cyan
|
|
47
|
+
INFO: '\x1b[32m', // green
|
|
48
|
+
WARN: '\x1b[33m', // yellow
|
|
49
|
+
ERROR: '\x1b[31m', // red
|
|
50
|
+
FATAL: '\x1b[35m' // magenta
|
|
51
|
+
};
|
|
52
|
+
return colors[level] || '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { logger };
|