Skip to content

Commit d268e21

Browse files
committed
feat(release-tracks): implement mongoose models for release track registry and snapshots
1 parent 9434938 commit d268e21

File tree

3 files changed

+543
-0
lines changed

3 files changed

+543
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use strict';
2+
3+
const mongoose = require('mongoose');
4+
const logger = require('../../lib/logger');
5+
const { releaseTrackSnapshotSchema } = require('./release-track-snapshot-schema');
6+
7+
/**
8+
* ModelFactory manages dynamic Mongoose model creation and caching for release tracks.
9+
*
10+
* Each release track gets its own MongoDB collection (named by track_id, e.g. "release-track--<uuid>").
11+
* This factory creates Mongoose models on demand and caches them so that repeated access
12+
* to the same track reuses the same compiled model.
13+
*/
14+
class ModelFactory {
15+
constructor() {
16+
/** @type {Map<string, mongoose.Model>} */
17+
this._cache = new Map();
18+
}
19+
20+
/**
21+
* Get or create a cached Mongoose model for a release track collection.
22+
*
23+
* @param {string} trackId - The release track ID (e.g. "release-track--<uuid>"),
24+
* which is also used as the MongoDB collection name.
25+
* @returns {mongoose.Model} The Mongoose model bound to the track's collection.
26+
*/
27+
getModel(trackId) {
28+
if (this._cache.has(trackId)) {
29+
return this._cache.get(trackId);
30+
}
31+
32+
// Mongoose model names must be unique per connection. Use the trackId directly
33+
// since it's already globally unique (release-track--<uuid>).
34+
const model = mongoose.model(trackId, releaseTrackSnapshotSchema, trackId);
35+
this._cache.set(trackId, model);
36+
37+
logger.verbose(`ModelFactory: Created model for collection "${trackId}"`);
38+
return model;
39+
}
40+
41+
/**
42+
* Remove a cached model. Call this when a release track is deleted
43+
* so the model doesn't linger in memory.
44+
*
45+
* @param {string} trackId - The release track ID to remove from cache.
46+
*/
47+
removeModel(trackId) {
48+
if (this._cache.has(trackId)) {
49+
// Remove from Mongoose's internal model registry
50+
delete mongoose.connection.models[trackId];
51+
this._cache.delete(trackId);
52+
logger.verbose(`ModelFactory: Removed model for collection "${trackId}"`);
53+
}
54+
}
55+
56+
/**
57+
* Ensure indexes are created on a release track's collection.
58+
* Call this after creating a new track to build the indexes defined in the schema.
59+
*
60+
* @param {string} trackId - The release track ID.
61+
* @returns {Promise<void>}
62+
*/
63+
async ensureIndexes(trackId) {
64+
const model = this.getModel(trackId);
65+
await model.ensureIndexes();
66+
logger.verbose(`ModelFactory: Ensured indexes for collection "${trackId}"`);
67+
}
68+
}
69+
70+
// Singleton instance -- shared across the application
71+
const modelFactory = new ModelFactory();
72+
73+
module.exports = modelFactory;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use strict';
2+
3+
const mongoose = require('mongoose');
4+
5+
// --- Validation patterns ---
6+
7+
const TRACK_ID_RE = /^release-track--[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
8+
const TRACK_NAME_RE = /^[a-zA-Z0-9 ]+$/;
9+
const VERSION_RE = /^\d+\.\d+$/;
10+
const CRON_RE = /^(\S+\s+){4}\S+$/;
11+
12+
// --- Sub-schemas ---
13+
14+
const snapshotScheduleDefinition = {
15+
mode: {
16+
type: String,
17+
enum: ['manual', 'cron', 'dates'],
18+
default: 'manual',
19+
},
20+
cron: {
21+
type: String,
22+
validate: {
23+
validator: (v) => CRON_RE.test(v),
24+
message: (props) => `"${props.value}" is not a valid cron expression (expected 5 fields)`,
25+
},
26+
},
27+
dates: { type: [Date], default: undefined },
28+
};
29+
const snapshotScheduleSchema = new mongoose.Schema(snapshotScheduleDefinition, { _id: false });
30+
31+
// --- Registry document definition ---
32+
33+
const releaseTrackRegistryDefinition = {
34+
track_id: {
35+
type: String,
36+
required: [true, 'Release track ID is required'],
37+
index: { unique: true },
38+
validate: {
39+
validator: (v) => TRACK_ID_RE.test(v),
40+
message: (props) =>
41+
`"${props.value}" is not a valid release track ID (expected "release-track--<uuid>")`,
42+
},
43+
},
44+
type: {
45+
type: String,
46+
enum: ['standard', 'virtual'],
47+
required: true,
48+
},
49+
name: {
50+
type: String,
51+
required: [true, 'Release track name is required'],
52+
validate: {
53+
validator: (v) => TRACK_NAME_RE.test(v),
54+
message: (props) =>
55+
`"${props.value}" is not a valid release track name (only alphanumeric characters and spaces allowed)`,
56+
},
57+
},
58+
description: { type: String },
59+
60+
// Denormalized for fast listing (updated on each snapshot/tag)
61+
latest_snapshot_modified: { type: Date },
62+
latest_tagged_version: {
63+
type: String,
64+
default: null,
65+
validate: {
66+
validator: (v) => v === null || VERSION_RE.test(v),
67+
message: (props) =>
68+
`"${props.value}" is not a valid version (expected MAJOR.MINOR format, e.g. "1.0")`,
69+
},
70+
},
71+
snapshot_count: { type: Number, default: 0 },
72+
tagged_release_count: { type: Number, default: 0 },
73+
74+
// Virtual tracks only
75+
snapshot_schedule: { type: snapshotScheduleSchema, default: undefined },
76+
77+
created_at: { type: Date, required: true },
78+
updated_at: { type: Date, required: true },
79+
};
80+
81+
// --- Schema creation ---
82+
83+
const releaseTrackRegistrySchema = new mongoose.Schema(releaseTrackRegistryDefinition, {
84+
collection: 'releaseTrackRegistry',
85+
bufferCommands: false,
86+
});
87+
88+
// --- Indexes ---
89+
90+
releaseTrackRegistrySchema.index({ type: 1 });
91+
92+
// --- Model creation ---
93+
94+
const ReleaseTrackRegistryModel = mongoose.model(
95+
'ReleaseTrackRegistry',
96+
releaseTrackRegistrySchema,
97+
);
98+
99+
module.exports = ReleaseTrackRegistryModel;

0 commit comments

Comments
 (0)