UNPKG

localgoose

Version:

A lightweight, file-based ODM Database for Node.js, inspired by Mongoose

1,318 lines (1,111 loc) 55.9 kB
/** * Localgoose Full Test Suite * Tests all major features: Schema, Model, Query, Document, Aggregate, Middleware, Population */ const { localgoose } = require('../src/index.js'); const fs = require('fs'); const path = require('path'); const DB_PATH = './test_db'; const assertModule = require('assert'); function assert(condition, msg) { assertModule.ok(condition, msg); } function assertEqual(a, b, msg) { assertModule.strictEqual(a, b, msg); } function assertNull(a, msg) { assertModule.strictEqual(a, null, msg); } function assertArray(a, msg) { assertModule.ok(Array.isArray(a), msg); } async function cleanup(db) { try { db.disconnect(); } catch (e) {} try { fs.rmSync(DB_PATH, { recursive: true, force: true }); } catch (e) {} } // ─── Schemas used across tests ─────────────────────────────────────────────── function buildSchemas(db) { const authorSchema = new localgoose.Schema({ name: { type: String, required: true }, email: { type: String }, age: { type: Number, min: [0, 'Age must be non-negative'], max: 120 }, score: { type: Number, default: 0 }, tags: { type: Array, default: [] }, active:{ type: Boolean, default: true }, rating:{ type: Number }, profile: { type: Object, default: {} } }); authorSchema.virtual('displayName').get(function () { return `${this.name} <${this.email}>`; }); authorSchema.method('greet', function () { return `Hello, I am ${this.name}`; }); authorSchema.static('findActive', function () { return this.find({ active: true }); }); authorSchema.pre('save', async function () { this._preSaveCalled = true; }); authorSchema.post('save', async function () { this._postSaveCalled = true; }); const postSchema = new localgoose.Schema({ title: { type: String, required: true, minlength: 3, maxlength: 200 }, content: { type: String }, likes: { type: Number, default: 0 }, tags: { type: Array, default: [] }, author: { type: String, ref: 'SuiteAuthor' }, published: { type: Boolean, default: false }, meta: { type: Object, default: {} } }); const Author = db.model('SuiteAuthor', authorSchema); const Post = db.model('SuitePost', postSchema); return { Author, Post, authorSchema, postSchema }; } // ══════════════════════════════════════════════════════════════════════════════ describe('Localgoose Full Suite', () => { let db, Author, Post; beforeAll(async () => { if (fs.existsSync(DB_PATH)) fs.rmSync(DB_PATH, { recursive: true, force: true }); db = await localgoose.connect(DB_PATH); const schemas = buildSchemas(db); Author = schemas.Author; Post = schemas.Post; }); afterAll(async () => { await cleanup(db); }); // ── 1. Schema ──────────────────────────────────────────────────────────── console.log('\n📋 1. Schema'); test('Schema.Types exposed', async () => { assert(localgoose.Schema.Types.String === String); assert(localgoose.Schema.Types.Number === Number); assert(localgoose.Schema.Types.ObjectId); }); test('Schema virtual', async () => { const s = new localgoose.Schema({ name: String, email: String }); s.virtual('display').get(function () { return `${this.name}/${this.email}`; }); assert(s.virtuals.display); }); test('Schema.method / statics', async () => { const s = new localgoose.Schema({ x: Number }); s.method('double', function () { return this.x * 2; }); s.static('findBig', function () { return this.find({ x: { $gt: 100 } }); }); assert(typeof s.methods.double === 'function'); assert(typeof s.statics.findBig === 'function'); }); test('Schema pre/post hooks register without throw', async () => { const s = new localgoose.Schema({ n: String }); ['save','validate','remove','deleteOne','deleteMany','find','findOne', 'findOneAndUpdate','updateOne','updateMany','aggregate','insertMany'].forEach(h => { s.pre(h, async function () {}); s.post(h, async function () {}); }); }); test('Schema.pre throws on invalid hook', async () => { const s = new localgoose.Schema({}); let threw = false; try { s.pre('badHook', () => {}); } catch (e) { threw = true; } assert(threw, 'Should throw for invalid hook'); }); test('Schema validate — required', async () => { const s = new localgoose.Schema({ name: { type: String, required: true } }); const errs = s.validate({}); assert(errs.length > 0, 'Should fail required'); }); test('Schema validate — min/max (array form)', async () => { const s = new localgoose.Schema({ age: { type: Number, min: [18, 'Too young'], max: [65, 'Too old'] } }); const e1 = s.validate({ age: 10 }); assert(e1[0] === 'Too young', `Got: ${e1[0]}`); const e2 = s.validate({ age: 70 }); assert(e2[0] === 'Too old', `Got: ${e2[0]}`); const e3 = s.validate({ age: 30 }); assertEqual(e3.length, 0, 'Should pass'); }); test('Schema validate — minlength/maxlength', async () => { const s = new localgoose.Schema({ title: { type: String, minlength: 5, maxlength: 10 } }); assert(s.validate({ title: 'Hi' }).length > 0, 'minlength fail'); assert(s.validate({ title: 'A very long title indeed' }).length > 0, 'maxlength fail'); assert(s.validate({ title: 'Hello' }).length === 0, 'should pass'); }); test('Schema validate — enum', async () => { const s = new localgoose.Schema({ status: { type: String, enum: ['a', 'b'] } }); assert(s.validate({ status: 'c' }).length > 0, 'enum fail'); assert(s.validate({ status: 'a' }).length === 0, 'enum pass'); }); test('Schema validate — custom validator', async () => { const s = new localgoose.Schema({ score: { type: Number, validate: { validator: v => v >= 0, message: 'Must be non-negative' } } }); assert(s.validate({ score: -1 }).length > 0, 'custom validator fail'); assert(s.validate({ score: 5 }).length === 0, 'custom validator pass'); }); test('Schema.add(schema) merges paths', async () => { const base = new localgoose.Schema({ createdBy: String }); const ext = new localgoose.Schema({ title: String }); ext.add(base); assert(ext._paths.has('createdBy'), 'Should have createdBy'); assert(ext._paths.has('title'), 'Should have title'); }); test('Schema.clone()', async () => { const s = new localgoose.Schema({ x: Number }); s.method('foo', () => 1); const c = s.clone(); assert(c._paths.has('x')); assert(c.methods.foo); assert(c !== s); }); test('Schema allows custom class types', async () => { class GeoPoint {} const s = new localgoose.Schema({ loc: { type: GeoPoint } }); assert(s); }); test('Schema.eachPath iterates paths', async () => { const s = new localgoose.Schema({ a: String, b: Number }); const paths = []; s.eachPath((p) => paths.push(p)); assert(paths.includes('a') && paths.includes('b')); }); test('Schema.path() returns SchemaType', async () => { const s = new localgoose.Schema({ age: { type: Number, min: 0 } }); assert(s.path('age')); }); test('Schema.indexes()', async () => { const s = new localgoose.Schema({ email: { type: String, index: true } }); assert(s.indexes().length > 0); }); test('Schema.pick()', async () => { const s = new localgoose.Schema({ a: String, b: Number, c: Boolean }); const p = s.pick(['a','b']); assert(p._paths.has('a') && p._paths.has('b') && !p._paths.has('c')); }); test('Schema.omit()', async () => { const s = new localgoose.Schema({ a: String, b: Number, c: Boolean }); const o = s.omit(['c']); assert(o._paths.has('a') && !o._paths.has('c')); }); test('Schema toJSONSchema()', async () => { const s = new localgoose.Schema({ name: { type: String, required: true }, age: Number }); const js = s.toJSONSchema(); assert(js.type === 'object'); assert(js.properties.name); }); test('Schema.plugin()', async () => { const s = new localgoose.Schema({ x: Number }); let called = false; s.plugin(function (schema) { called = true; schema.method('pluginMethod', () => 42); }); assert(called && s.methods.pluginMethod); }); // ── 2. CRUD ────────────────────────────────────────────────────────────── console.log('\n📝 2. CRUD Operations'); let alice, bob, carol, dave; test('create() single doc', async () => { alice = await Author.create({ name: 'Alice', email: 'alice@t.com', age: 28, tags: ['js', 'ts'], rating: 4.5 }); assert(alice._id, 'Has _id'); assertEqual(alice.name, 'Alice'); assertEqual(alice.score, 0, 'Default applied'); }); test('create() multiple docs', async () => { [bob, carol, dave] = await Author.create([ { name: 'Bob', email: 'bob@t.com', age: 22, tags: ['python'], rating: 3.2 }, { name: 'Carol', email: 'carol@t.com', age: 35, tags: ['js', 'css'], rating: 4.8 }, { name: 'Dave', email: 'dave@t.com', age: 22, tags: ['rust'], rating: 2.1 } ]); assert(bob && carol && dave, 'All created'); }); test('find() all', async () => { const docs = await Author.find().exec(); assertEqual(docs.length, 4); }); test('findOne() hit', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); assert(doc !== null); assertEqual(doc.name, 'Alice'); }); test('findOne() miss returns null', async () => { const doc = await Author.findOne({ name: 'Nobody' }).exec(); assertNull(doc, 'Should be null'); }); test('findById() returns chainable Query', async () => { const doc = await Author.findById(alice._id).exec(); assert(doc && doc.name === 'Alice'); // Confirm it's chainable const doc2 = await Author.findById(alice._id).lean().exec(); assert(doc2 && doc2.name === 'Alice'); }); test('find() with conditions', async () => { const docs = await Author.find({ age: 22 }).exec(); assertEqual(docs.length, 2); assert(docs.every(d => d.age === 22)); }); test('find() with $gt / $lt', async () => { const docs = await Author.find({ age: { $gt: 22, $lt: 35 } }).exec(); assertEqual(docs.length, 1); assertEqual(docs[0].name, 'Alice'); }); test('find() with $in', async () => { const docs = await Author.find({ name: { $in: ['Alice', 'Carol'] } }).exec(); assertEqual(docs.length, 2); }); test('find() with $nin', async () => { const docs = await Author.find({ name: { $nin: ['Alice', 'Carol'] } }).exec(); assertEqual(docs.length, 2); }); test('find() with $ne', async () => { const docs = await Author.find({ age: { $ne: 22 } }).exec(); assert(docs.every(d => d.age !== 22)); }); test('find() with $regex', async () => { const docs = await Author.find({ email: { $regex: /alice/i } }).exec(); assertEqual(docs.length, 1); assertEqual(docs[0].name, 'Alice'); }); test('find() with $or', async () => { const docs = await Author.find({ $or: [{ name: 'Alice' }, { name: 'Bob' }] }).exec(); assertEqual(docs.length, 2); }); test('find() with $and', async () => { const docs = await Author.find({ $and: [{ age: { $gte: 22 } }, { name: { $regex: /^A/ } }] }).exec(); assertEqual(docs.length, 1); assertEqual(docs[0].name, 'Alice'); }); test('find() with $nor', async () => { const docs = await Author.find({ $nor: [{ name: 'Alice' }, { name: 'Bob' }] }).exec(); assert(docs.every(d => d.name !== 'Alice' && d.name !== 'Bob')); }); test('find() dot-notation nested field', async () => { await Author.updateOne({ name: 'Alice' }, { $set: { 'profile.city': 'NYC' } }); const docs = await Author.find({ 'profile.city': 'NYC' }).exec(); assertEqual(docs.length, 1); }); test('find() with $size on array', async () => { const docs = await Author.find({ tags: { $size: 2 } }).exec(); assert(docs.every(d => d.tags.length === 2)); }); test('find() with $all on array', async () => { const docs = await Author.find({ tags: { $all: ['js', 'css'] } }).exec(); assertEqual(docs.length, 1); assertEqual(docs[0].name, 'Carol'); }); test('find() with $elemMatch (object)', async () => { const docs = await Author.find({ tags: { $elemMatch: { $eq: 'rust' } } }).exec(); assertEqual(docs.length, 1); assertEqual(docs[0].name, 'Dave'); }); test('find() with $exists', async () => { const docs = await Author.find({ rating: { $exists: true } }).exec(); assertEqual(docs.length, 4); }); test('find() with $not', async () => { const docs = await Author.find({ age: { $not: { $eq: 22 } } }).exec(); assert(docs.every(d => d.age !== 22)); }); test('sort() descending', async () => { const docs = await Author.find().sort({ age: -1 }).exec(); assertEqual(docs[0].name, 'Carol'); }); test('sort() string form', async () => { const docs = await Author.find().sort('-age').exec(); assertEqual(docs[0].name, 'Carol'); }); test('limit()', async () => { const docs = await Author.find().limit(2).exec(); assertEqual(docs.length, 2); }); test('skip()', async () => { const all = await Author.find().sort({ name: 1 }).exec(); const skipped = await Author.find().sort({ name: 1 }).skip(2).exec(); assertEqual(skipped[0].name, all[2].name); }); test('paginate()', async () => { const docs = await Author.find().sort({ name: 1 }).paginate(2, 2).exec(); assertEqual(docs.length, 2); }); test('select() inclusive projection', async () => { const docs = await Author.find({ name: 'Alice' }).select('name email').exec(); assert(docs[0].name, 'name present'); assert(docs[0].email, 'email present'); assertEqual(docs[0].age, undefined, 'age excluded'); }); test('select() exclusive projection', async () => { const docs = await Author.find({ name: 'Alice' }).select('-score -tags').exec(); assertEqual(docs[0].score, undefined, 'score excluded'); assertEqual(docs[0].tags, undefined, 'tags excluded'); assert(docs[0].name, 'name still present'); }); test('lean() returns plain objects', async () => { const docs = await Author.find({ name: 'Alice' }).lean().exec(); assertEqual(typeof docs[0].toObject, 'undefined', 'No toObject on lean'); assert(docs[0].name === 'Alice'); }); test('where().gt().lt() chaining on same field', async () => { const docs = await Author.find().where('age').gt(22).lt(35).exec(); assertEqual(docs.length, 1); assertEqual(docs[0].name, 'Alice'); }); test('where().in()', async () => { const docs = await Author.find().where('name').in(['Alice', 'Dave']).exec(); assertEqual(docs.length, 2); }); test('where().regex()', async () => { const docs = await Author.find().where('name').regex(/^[AC]/).exec(); assertEqual(docs.length, 2); }); test('where().ne()', async () => { const docs = await Author.find().where('age').ne(22).exec(); assert(docs.every(d => d.age !== 22)); }); test('countDocuments()', async () => { const n = await Author.countDocuments({ age: 22 }); assertEqual(n, 2); }); test('estimatedDocumentCount()', async () => { const n = await Author.estimatedDocumentCount(); assert(n >= 4); }); test('distinct()', async () => { const ages = await Author.distinct('age'); assert(ages.includes(22) && ages.includes(28) && ages.includes(35)); assert(ages.filter(a => a === 22).length === 1, 'No duplicates'); }); test('exists() returns {_id} or null', async () => { const found = await Author.exists({ name: 'Alice' }); assert(found && found._id, 'found._id present'); const notFound = await Author.exists({ name: 'Ghost' }); assertNull(notFound, 'Should be null'); }); // ── 3. Updates ─────────────────────────────────────────────────────────── console.log('\n✏️ 3. Update Operations'); test('updateOne() $set', async () => { await Author.updateOne({ name: 'Alice' }, { $set: { score: 99 } }); const doc = await Author.findOne({ name: 'Alice' }).exec(); assertEqual(doc.score, 99); }); test('updateOne() $inc', async () => { await Author.updateOne({ name: 'Bob' }, { $inc: { score: 5 } }); const doc = await Author.findOne({ name: 'Bob' }).exec(); assertEqual(doc.score, 5); }); test('updateOne() $inc again', async () => { await Author.updateOne({ name: 'Bob' }, { $inc: { score: 10 } }); const doc = await Author.findOne({ name: 'Bob' }).exec(); assertEqual(doc.score, 15); }); test('updateOne() $mul', async () => { await Author.updateOne({ name: 'Alice' }, { $mul: { score: 2 } }); const doc = await Author.findOne({ name: 'Alice' }).exec(); assertEqual(doc.score, 198); }); test('updateOne() $min / $max', async () => { await Author.updateOne({ name: 'Carol' }, { $min: { score: 50 }, $max: { score: 50 } }); const doc = await Author.findOne({ name: 'Carol' }).exec(); assertEqual(doc.score, 50); }); test('updateOne() $unset removes field', async () => { await Author.updateOne({ name: 'Dave' }, { $set: { score: 77 } }); await Author.updateOne({ name: 'Dave' }, { $unset: { score: 1 } }); const doc = await Author.findOne({ name: 'Dave' }).exec(); assertEqual(doc.score, undefined); }); test('updateOne() $rename', async () => { await Author.updateOne({ name: 'Dave' }, { $set: { tmpField: 'hello' } }); await Author.updateOne({ name: 'Dave' }, { $rename: { tmpField: 'renamedField' } }); const doc = await Author.findOne({ name: 'Dave' }).exec(); assertEqual(doc.renamedField, 'hello'); assertEqual(doc.tmpField, undefined); }); test('updateOne() $push', async () => { await Author.updateOne({ name: 'Alice' }, { $push: { tags: 'node' } }); const doc = await Author.findOne({ name: 'Alice' }).exec(); assert(doc.tags.includes('node')); }); test('updateOne() $push $each', async () => { await Author.updateOne({ name: 'Bob' }, { $push: { tags: { $each: ['go', 'ruby'] } } }); const doc = await Author.findOne({ name: 'Bob' }).exec(); assert(doc.tags.includes('go') && doc.tags.includes('ruby')); }); test('updateOne() $pull', async () => { await Author.updateOne({ name: 'Alice' }, { $pull: { tags: 'node' } }); const doc = await Author.findOne({ name: 'Alice' }).exec(); assert(!doc.tags.includes('node')); }); test('updateOne() $addToSet no duplicates', async () => { await Author.updateOne({ name: 'Alice' }, { $addToSet: { tags: 'js' } }); const doc = await Author.findOne({ name: 'Alice' }).exec(); assertEqual(doc.tags.filter(t => t === 'js').length, 1); }); test('updateOne() $addToSet $each', async () => { await Author.updateOne({ name: 'Carol' }, { $addToSet: { tags: { $each: ['node', 'ts'] } } }); const doc = await Author.findOne({ name: 'Carol' }).exec(); assert(doc.tags.includes('node') && doc.tags.includes('ts')); }); test('updateOne() $pop last', async () => { await Author.updateOne({ name: 'Bob' }, { $pop: { tags: 1 } }); const doc = await Author.findOne({ name: 'Bob' }).exec(); assert(!doc.tags.includes('ruby')); }); test('updateOne() $currentDate', async () => { await Author.updateOne({ name: 'Alice' }, { $currentDate: { updatedAt: true } }); const doc = await Author.findOne({ name: 'Alice' }).exec(); assert(doc.updatedAt instanceof Date || typeof doc.updatedAt === 'string'); }); test('updateMany()', async () => { const res = await Author.updateMany({ age: 22 }, { $set: { active: false } }); assertEqual(res.modifiedCount, 2); const docs = await Author.find({ active: false }).exec(); assertEqual(docs.length, 2); }); test('updateOne() upsert creates doc', async () => { const res = await Author.updateOne({ name: 'Eve' }, { $set: { name: 'Eve', email: 'eve@t.com', age: 25 } }, { upsert: true }); assert(res.upsertedCount === 1 || res.modifiedCount >= 0); const doc = await Author.findOne({ name: 'Eve' }).exec(); assert(doc !== null); await Author.deleteOne({ name: 'Eve' }); }); test('findOneAndUpdate() returns updated doc', async () => { const doc = await Author.findOneAndUpdate({ name: 'Alice' }, { $set: { score: 200 } }, { new: true }); assertEqual(doc.score, 200); }); test('findOneAndUpdate() returns old doc with {new:false}', async () => { const before = await Author.findOne({ name: 'Alice' }).exec(); const doc = await Author.findOneAndUpdate({ name: 'Alice' }, { $set: { score: 300 } }, { new: false }); assertEqual(doc.score, before.score); }); test('findByIdAndUpdate()', async () => { const doc = await Author.findByIdAndUpdate(alice._id, { $set: { score: 111 } }, { new: true }); assertEqual(doc.score, 111); }); test('replaceOne()', async () => { await Author.create({ name: 'Temp', email: 'tmp@t.com', age: 99 }); await Author.replaceOne({ name: 'Temp' }, { name: 'Replaced', email: 'r@t.com', age: 50 }); const doc = await Author.findOne({ name: 'Replaced' }).exec(); assert(doc !== null); await Author.deleteOne({ name: 'Replaced' }); }); // ── 4. Delete ──────────────────────────────────────────────────────────── console.log('\n🗑️ 4. Delete Operations'); test('deleteOne()', async () => { await Author.create({ name: 'Del1', email: 'd1@t.com', age: 10 }); const res = await Author.deleteOne({ name: 'Del1' }); assertEqual(res.deletedCount, 1); assertNull(await Author.findOne({ name: 'Del1' }).exec()); }); test('deleteMany()', async () => { await Author.create([ { name: 'DelA', email: 'da@t.com', age: 5 }, { name: 'DelB', email: 'db@t.com', age: 5 } ]); const res = await Author.deleteMany({ age: 5 }); assertEqual(res.deletedCount, 2); }); test('findOneAndDelete()', async () => { await Author.create({ name: 'DelC', email: 'dc@t.com', age: 44 }); const doc = await Author.findOneAndDelete({ name: 'DelC' }); assert(doc && doc.name === 'DelC'); assertNull(await Author.findOne({ name: 'DelC' }).exec()); }); test('findByIdAndDelete()', async () => { const tmp = await Author.create({ name: 'DelD', email: 'dd@t.com', age: 55 }); const doc = await Author.findByIdAndDelete(tmp._id); assert(doc !== null); assertNull(await Author.findOne({ name: 'DelD' }).exec()); }); // ── 5. Document Instance ───────────────────────────────────────────────── console.log('\n📄 5. Document Instance'); test('Document.save() inserts new doc', async () => { const AuthorModel = db.model('SuiteAuthor'); // Using standard Mongoose Constructor format const doc = new AuthorModel({ name: 'NewSave', email: 'ns@t.com', age: 33 }); await doc.save(); const found = await Author.findOne({ name: 'NewSave' }).exec(); assert(found !== null); await Author.deleteOne({ name: 'NewSave' }); }); test('Document.save() updates existing doc', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); doc.set('score', 999); await doc.save(); const updated = await Author.findOne({ name: 'Alice' }).exec(); assertEqual(updated.score, 999); }); test('Document.get() / set()', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); doc.set('score', 42); assertEqual(doc.get('score'), 42); }); test('Document.isModified()', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); const was = doc.isModified('score'); doc.set('score', 1); assert(doc.isModified('score')); }); test('Document.modifiedPaths()', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); doc.set('score', 55); doc.set('age', 99); const paths = doc.modifiedPaths(); assert(paths.includes('score') && paths.includes('age')); }); test('Document.toObject()', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); const obj = doc.toObject(); assert(typeof obj === 'object'); assert(obj._id); }); test('Document.toObject({ virtuals: true }) includes virtuals', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); const obj = doc.toObject({ virtuals: true }); assert(obj.displayName, `displayName: ${obj.displayName}`); assert(obj.displayName.includes('Alice')); }); test('Document.toJSON()', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); const json = doc.toJSON(); assert(json._id); }); test('Document instance method', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); assertEqual(doc.greet(), 'Hello, I am Alice'); }); test('Document virtual getter', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); assert(doc.displayName.includes('Alice')); }); test('Document.isNew = false after load', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); assert(!doc.isNew, 'Loaded doc should not be new'); }); test('Document.updateOne()', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); await doc.updateOne({ $set: { score: 77 } }); const updated = await Author.findOne({ name: 'Alice' }).exec(); assertEqual(updated.score, 77); }); test('Document.equals()', async () => { const a = await Author.findOne({ name: 'Alice' }).exec(); const b = await Author.findOne({ name: 'Alice' }).exec(); assert(a.equals(b)); }); test('Document.$inc()', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); doc.$inc('score', 10); assert(doc.get('score') > 77); }); test('Document.markModified()', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); doc.markModified('tags'); assert(doc.modifiedPaths().includes('tags')); }); test('Document pre-save middleware called', async () => { const doc = await Author.findOne({ name: 'Alice' }).exec(); doc.set('score', 100); await doc.save(); // pre-save sets _preSaveCalled on proxy; post-save on doc itself assert(true); // just confirm no crash }); test('Document.remove() deletes doc', async () => { const tmp = await Author.create({ name: 'TmpDel', email: 'tdel@t.com', age: 1 }); const doc = await Author.findOne({ name: 'TmpDel' }).exec(); await doc.remove(); assertNull(await Author.findOne({ name: 'TmpDel' }).exec()); }); test('Document unique constraints', async () => { const UniqueModel = db.model('UniqueTest', new localgoose.Schema({ uniqueKey: { type: String, unique: true } })); await UniqueModel.deleteMany({}); await UniqueModel.create({ uniqueKey: '123' }); let threw = false; try { await UniqueModel.create({ uniqueKey: '123' }); } catch (e) { threw = true; assert(e.code === 11000, `Expected code 11000, got ${e.code}`); } assert(threw, 'Unique constraint should violently throw'); await UniqueModel.deleteMany({}); }); // ── 6. Population ──────────────────────────────────────────────────────── console.log('\n🔗 6. Population'); let post1, post2; test('create posts with author refs', async () => { const freshAlice = await Author.findOne({ name: 'Alice' }).exec(); post1 = await Post.create({ title: 'Into to JS', content: 'JS basics', author: freshAlice._id, likes: 10, tags: ['js'] }); post2 = await Post.create({ title: 'Advanced TS', content: 'TypeScript tips', author: freshAlice._id, likes: 25, tags: ['ts'] }); assert(post1._id && post2._id); }); test('populate() resolves ref', async () => { const doc = await Post.findOne({ title: 'Into to JS' }).populate('author').exec(); assert(doc.author && doc.author.name === 'Alice', `Expected Alice, got: ${doc.author}`); }); test('populate() with select', async () => { const doc = await Post.findOne({ title: 'Into to JS' }) .populate({ path: 'author', select: 'name' }) .exec(); assert(doc.author && doc.author.name, 'has name'); assertEqual(doc.author.age, undefined, 'age excluded'); }); test('Document.populate() instance method', async () => { const doc = await Post.findOne({ title: 'Into to JS' }).exec(); await doc.populate('author'); assert(doc.author && doc.author.name === 'Alice'); }); test('Model.populate() static', async () => { const docs = await Post.find({ title: 'Into to JS' }).exec(); // static populate not easily testable without ref config; just confirm no crash assert(docs.length > 0); }); // ── 7. bulkWrite ───────────────────────────────────────────────────────── console.log('\n📦 7. bulkWrite'); test('bulkWrite insertOne', async () => { const res = await Author.bulkWrite([ { insertOne: { document: { name: 'BulkA', email: 'ba@t.com', age: 20 } } } ]); assertEqual(res.insertedCount, 1); assert(await Author.findOne({ name: 'BulkA' }).exec()); }); test('bulkWrite updateOne', async () => { const res = await Author.bulkWrite([ { updateOne: { filter: { name: 'BulkA' }, update: { $set: { score: 55 } } } } ]); assertEqual(res.modifiedCount, 1); const doc = await Author.findOne({ name: 'BulkA' }).exec(); assertEqual(doc.score, 55); }); test('bulkWrite deleteOne', async () => { const res = await Author.bulkWrite([{ deleteOne: { filter: { name: 'BulkA' } } }]); assertEqual(res.deletedCount, 1); }); test('bulkWrite mixed operations', async () => { const res = await Author.bulkWrite([ { insertOne: { document: { name: 'BwX', email: 'bwx@t.com', age: 1 } } }, { insertOne: { document: { name: 'BwY', email: 'bwy@t.com', age: 2 } } }, { updateOne: { filter: { name: 'BwX' }, update: { $set: { score: 10 } } } }, { deleteOne: { filter: { name: 'BwY' } } } ]); assertEqual(res.insertedCount, 2); assertEqual(res.modifiedCount, 1); assertEqual(res.deletedCount, 1); await Author.deleteOne({ name: 'BwX' }); }); // ── 8. Aggregation ─────────────────────────────────────────────────────── console.log('\n📊 8. Aggregation'); test('aggregate $match', async () => { const res = await Author.aggregate().match({ active: true }).exec(); assert(res.length > 0); assert(res.every(d => d.active === true)); }); test('aggregate $group $sum', async () => { const res = await Author.aggregate().group({ _id: null, total: { $sum: '$score' } }).exec(); assert(typeof res[0].total === 'number'); }); test('aggregate $group $avg returns number', async () => { const res = await Author.aggregate().group({ _id: null, avgAge: { $avg: '$age' } }).exec(); assert(typeof res[0].avgAge === 'number', `Expected number, got ${typeof res[0].avgAge}`); }); test('aggregate $group $min $max', async () => { const res = await Author.aggregate().group({ _id: null, minAge: { $min: '$age' }, maxAge: { $max: '$age' } }).exec(); assert(res[0].minAge <= res[0].maxAge); }); test('aggregate $group $push', async () => { const res = await Author.aggregate().group({ _id: null, names: { $push: '$name' } }).exec(); assertArray(res[0].names); assert(res[0].names.includes('Alice')); }); test('aggregate $group $addToSet', async () => { const res = await Author.aggregate().group({ _id: null, uniqueAges: { $addToSet: '$age' } }).exec(); assertArray(res[0].uniqueAges); // no duplicates const ages = res[0].uniqueAges; assertEqual(ages.length, new Set(ages).size); }); test('aggregate $group $first $last', async () => { const res = await Author.aggregate() .sort({ age: 1 }) .group({ _id: null, youngest: { $first: '$name' }, oldest: { $last: '$name' } }) .exec(); assert(res[0].youngest && res[0].oldest); }); test('aggregate $group by field', async () => { const res = await Author.aggregate().group({ _id: '$age', count: { $sum: 1 } }).exec(); assert(res.length > 0); assert(res.every(r => r.count >= 1)); }); test('aggregate $sort', async () => { const res = await Author.aggregate().sort({ age: -1 }).exec(); for (let i = 1; i < res.length; i++) assert(res[i-1].age >= res[i].age, 'Descending'); }); test('aggregate $limit $skip', async () => { const res = await Author.aggregate().sort({ name: 1 }).skip(1).limit(2).exec(); assertEqual(res.length, 2); }); test('aggregate $project', async () => { const res = await Author.aggregate().project({ name: 1, age: 1 }).exec(); assert(res.every(d => d.name && d.age !== undefined)); assert(res.every(d => d.email === undefined)); }); test('aggregate $addFields', async () => { const res = await Author.aggregate() .match({ name: 'Alice' }) .addFields({ nameUpper: { $toUpper: '$name' } }) .exec(); assertEqual(res[0].nameUpper, 'ALICE'); }); test('aggregate $unwind', async () => { const res = await Author.aggregate() .match({ name: 'Carol' }) .unwind('$tags') .exec(); assert(res.length >= 2, `Expected >=2 unwind results, got ${res.length}`); assert(res.every(d => typeof d.tags === 'string')); }); test('aggregate $count', async () => { const res = await Author.aggregate().count('total').exec(); assert(res[0].total >= 4); }); test('aggregate $sample', async () => { const res = await Author.aggregate().sample(2).exec(); assertEqual(res.length, 2); }); test('aggregate $sortByCount', async () => { const res = await Author.aggregate().sortByCount('$age').exec(); assert(res.length > 0); assert(res[0].count >= res[res.length - 1].count); }); test('aggregate $lookup', async () => { const res = await Post.aggregate() .lookup({ from: 'SuiteAuthor', localField: 'author', foreignField: '_id', as: 'authorInfo' }) .exec(); assert(res.length > 0); assert(Array.isArray(res[0].authorInfo)); }); test('aggregate $facet', async () => { const res = await Author.aggregate() .facet({ byAge: [{ $group: { _id: '$age', count: { $sum: 1 } } }], total: [{ $count: 'n' }] }) .exec(); assert(res[0].byAge && Array.isArray(res[0].byAge)); assert(res[0].total && Array.isArray(res[0].total)); }); test('aggregate $set / $unset', async () => { const res = await Author.aggregate() .match({ name: 'Alice' }) .set({ bonus: { $add: ['$score', 100] } }) .unset(['score']) .exec(); assert(res[0].bonus !== undefined); assertEqual(res[0].score, undefined); }); test('aggregate $replaceRoot', async () => { const res = await Author.aggregate() .match({ name: 'Alice' }) .replaceRoot('$profile') .exec(); assert(res[0] !== undefined); }); test('aggregate expression $concat', async () => { const res = await Author.aggregate() .match({ name: 'Alice' }) .addFields({ fullLabel: { $concat: ['$name', ' - ', '$email'] } }) .exec(); assert(res[0].fullLabel.includes('Alice')); }); test('aggregate expression $cond', async () => { const res = await Author.aggregate() .addFields({ category: { $cond: { if: { $gte: ['$age', 30] }, then: 'senior', else: 'junior' } } }) .exec(); assert(res.every(d => d.category === 'senior' || d.category === 'junior')); }); test('aggregate expression $switch', async () => { const res = await Author.aggregate() .addFields({ tier: { $switch: { branches: [{ case: { $gte: ['$age', 35] }, then: 'A' }, { case: { $gte: ['$age', 28] }, then: 'B' }], default: 'C' } } }).exec(); assert(res.every(d => ['A','B','C'].includes(d.tier))); }); test('aggregate expression arithmetic ($subtract, $divide)', async () => { const res = await Author.aggregate() .match({ name: 'Alice' }) .addFields({ halfAge: { $divide: ['$age', 2] } }) .exec(); assertEqual(res[0].halfAge, 14); }); test('aggregate $group $mergeObjects', async () => { const res = await Author.aggregate() .group({ _id: null, merged: { $mergeObjects: '$profile' } }) .exec(); assert(res[0].merged !== undefined); }); test('aggregate $bucket', async () => { const res = await Author.aggregate() .bucket({ groupBy: '$age', boundaries: [0, 25, 35, 100], default: 'Other', output: { count: { $sum: 1 } } }) .exec(); assert(Array.isArray(res)); }); test('aggregate $bucketAuto', async () => { const res = await Author.aggregate().bucketAuto({ groupBy: '$age', buckets: 2 }).exec(); assert(res.length <= 2); }); test('aggregate then/catch (thenable)', async () => { const res = await Author.aggregate().match({ name: 'Alice' }); assert(Array.isArray(res)); }); test('aggregate pipeline() returns array copy', async () => { const agg = Author.aggregate().match({ x: 1 }).limit(10); const pl = agg.pipeline(); assertArray(pl); assertEqual(pl.length, 2); // Mutating copy does not affect original pl.push({ $skip: 1 }); assertEqual(agg.pipeline().length, 2); }); test('aggregate pre(aggregate) hook runs', async () => { let called = false; const s2 = new localgoose.Schema({ v: Number }); s2.pre('aggregate', async function () { called = true; }); const M2 = db.model('HookTest', s2); await M2.deleteMany({}); await M2.aggregate().match({}).exec(); assert(called, 'pre aggregate hook should have been called'); await M2.deleteMany({}); }); // ── 9. insertMany ──────────────────────────────────────────────────────── console.log('\n📥 9. insertMany'); test('insertMany() creates multiple docs', async () => { const docs = await Author.insertMany([ { name: 'IM1', email: 'im1@t.com', age: 10 }, { name: 'IM2', email: 'im2@t.com', age: 11 } ]); assertEqual(docs.length, 2); assert(docs[0]._id && docs[1]._id); await Author.deleteMany({ name: { $in: ['IM1', 'IM2'] } }); }); test('insertMany() with lean option', async () => { const docs = await Author.insertMany( [{ name: 'IML', email: 'iml@t.com', age: 5 }], { lean: true } ); assertEqual(typeof docs[0].toObject, 'undefined'); await Author.deleteOne({ name: 'IML' }); }); // ── 10. hydrate / castObject ────────────────────────────────────────────── console.log('\n🔧 10. Utility Methods'); test('hydrate() creates Document from plain obj', async () => { const raw = { _id: '123', name: 'Hydrated', email: 'h@t.com', age: 40, tags: [], score: 0, active: true, profile: {} }; const doc = Author.hydrate(raw); assert(doc.name === 'Hydrated'); assert(!doc.isNew); assert(typeof doc.toObject === 'function'); }); test('castObject()', async () => { const obj = Author.castObject({ name: 'Test', age: '25', score: '10' }); assert(obj.name === 'Test'); }); test('applyVirtuals()', async () => { const raw = { name: 'Alice', email: 'alice@t.com', age: 28, tags: [], score: 0 }; const v = Author.applyVirtuals(raw); assert(v.displayName); }); test('Model static method (findActive)', async () => { const docs = await Author.findActive().exec(); assert(Array.isArray(docs)); }); test('Query [Symbol.asyncIterator]', async () => { const names = []; for await (const doc of Author.find({ name: { $in: ['Alice', 'Carol'] } })) { names.push(doc.name); } assertEqual(names.length, 2); }); test('Aggregate [Symbol.asyncIterator]', async () => { const ages = []; for await (const doc of Author.aggregate().match({ name: 'Alice' })) { ages.push(doc.age); } assert(ages.length > 0); }); test('localgoose.Types exposed', async () => { assert(localgoose.Types.String === String); assert(localgoose.Types.Number === Number); assert(localgoose.Types.ObjectId); assert(localgoose.ObjectId); }); test('Connection.disconnect() returns Promise', async () => { const conn = await localgoose.connect('./tmp_conn_test'); const result = conn.disconnect(); assert(result && typeof result.then === 'function'); try { fs.rmSync('./tmp_conn_test', { recursive: true, force: true }); } catch (e) {} }); test('Connection.listCollections()', async () => { const list = db.listCollections(); assert(Array.isArray(list)); assert(list.some(c => c.name === 'SuiteAuthor')); }); test('Connection.modelNames()', async () => { const names = db.modelNames(); assert(names.includes('SuiteAuthor')); assert(names.includes('SuitePost')); }); // ── 11. Mongoose Parity Features ────────────────────────────────────────── console.log('\n🌟 11. Mongoose Parity'); test('Async Connection', async () => { const conn = await localgoose.connect(path.join(DB_PATH, 'async-test')); assertEqual(conn.readyState, 1); await conn.dropDatabase(); }); test('Ordered insertMany stops on first error', async () => { const User = db.model('ParityUser', new localgoose.Schema({ username: { type: String, unique: true } })); await User.deleteMany({}); try { await User.insertMany([ { username: 'u1' }, { username: 'u1' }, // Duplicate { username: 'u2' } ], { ordered: true }); } catch (err) { assertEqual(err.code, 11000); } const count = await User.countDocuments(); assertEqual(count, 1); }); test('Unordered insertMany continues on error', async () => { const User = db.model('ParityUserUnordered', new localgoose.Schema({ username: { type: String, unique: true } })); await User.deleteMany({}); await User.insertMany([ { username: 'u1' }, { username: 'u1' }, // Duplicate { username: 'u2' } ], { ordered: false }); const count = await User.countDocuments(); assertEqual(count, 2); }); test('Geospatial $near proximity matching', async () => { const Place = db.model('ParityPlace', new localgoose.Schema({ name: String, location: { type: { type: String, default: 'Point' }, coordinates: [Number] } })); await Place.create([ { name: 'A', location: { coordinates: [-73.9654, 40.7829] } }, { name: 'B', location: { coordinates: [-73.9851, 40.7589] } }, { name: 'C', location: { coordinates: [-73.9857, 40.7484] } } ]); const results = await Place.find({ location: { $near: [-73.9851, 40.7589], $maxDistance: 2000 } }); assert(results.length >= 2, 'Should find at least B and C'); }); test('Aggregation $geoNear', async () => { const Place = db.model('ParityPlaceAgg', new localgoose.Schema({ name: String, location: { type: { type: String, default: 'Point' }, coordinates: [Number] } })); await Place.create([ { name: 'X', location: { coordinates: [-73.9851, 40.7589] } }, { name: 'Y', location: { coordinates: [-73.9857, 40.7484] } } ]); const res = await Place.aggregate([ { $geoNear: { near: { type: "Point", coordinates: [-73.9851, 40.7589] }, distanceField: "dist", maxDistance: 5000 } } ]); assertEqual(res[0].name, 'X'); assertEqual(res[0].dist, 0); }); test('Refined $text search keywords', async () => { const Article = db.model('ParityArticle', new localgoose.Schema({ title: String })); await Article.create([{ title: 'Node.js Guide' }, { title: 'React Basics' }]); const res = await Article.find({ title: { $text: 'Guide Node.js' } }); assertEqual(res.length, 1); assertEqual(res[0].title, 'Node.js Guide'); const miss = await Article.find({ title: { $text: 'Node.js Python' } }); assertEqual(miss.length, 0); }); // ── 11b. README Parity & Advanced Features ──────────────────────────────── console.log('\n📖 11b. README Parity & Advanced Features'); test('Extended Schema Types (BigInt, Buffer, UUID)', async () => { const Schema = new localgoose.Schema({ val: BigInt, data: Buffer, uid: localgoose.Schema.Types.UUID }); const M = db.model('ExtTypeTest', Schema); const doc = await M.create({ val: '123', data: 'hello', uid: '550e8400-e29b-41d4-a716-446655440000' }); assertEqual(typeof doc.val, 'bigint'); assertEqual(doc.val, 123n); assert(doc.data instanceof Buffer); assertEqual(doc.data.toString(), 'hello'); assertEqual(doc.uid, '550e8400-e29b-41d4-a716-446655440000'); }); test('Query Operators ($mod, $nearSphere, findByIdAndRemove)', async () => { const ModModel = db.model('ParityModTest', new localgoose.Schema({ num: Number })); await ModModel.create([{ num: 10 }, { num: 11 }, { num: 20 }]); const modRes = await ModModel.find({ num: { $mod: [10, 0] } }); assertEqual(modRes.length, 2); const NearModel = db.model('ParityNearSphereTest', new localgoose.Schema({ loc: { type: { type: String, default: 'Point' }, coordinates: [Number] } })); await NearModel.create({ loc: { coordinates: [0, 0] } }); const nearRes = await NearModel.find({ loc: { $nearSphere: [0, 0], $maxDistance: 1000 } }); assertEqual(nearRes.length, 1); const RemModel = db.model('ParityRemoveTest', new localgoose.Schema({ name: String })); const rdoc = await RemModel.create({ name: 'removeme' }); const removed = await RemModel.findByIdAndRemove(rdoc._id); assertEqual(removed.name, 'removeme'); assertNull(await RemModel.findById(rdoc._id).exec()); }); test('$bit update operator', async () => { const M = db.model('ParityBitTest', new localgoose.Schema({ val: Number })); await M.create({ val: 0b1010 }); await M.updateOne({ val: 0b1010 }, { $bit: { val: { and: 0b1100, or: 0b0001 } } }); const doc = await M.findOne().exec(); assertEqual(doc.val, (0b1010 & 0b1100) | 0b0001); // 1001 = 9 }); test('Advanced Aggregation ($redact, $search)', async () => { const schema = new localgoose.Schema({ title: String, level: Number }); schema.index({ title: 'text' }); const M = db.model('ParityAggTest', schema); await M.create([{ title: 'Public News', level: 1 }, { title: 'Secret File', level: 5 }]); // $search stage (uses $text logic) const searchRes = await M.aggregate().search({ $search: 'Secret' }).exec(); assertEqual(searchRes.length, 1); assertEqual(searchRes[0].title, 'Secret File'); // $redact stage const redactRes = await M.aggregate() .redact({ $cond: { if: { $gt: ['$level', 2] }, then: '$$PRUNE', else: '$$DESCEND' } }).exec(); assertEqual(redactRes.length, 1); assertEqual(redactRes[0].title, 'Public News'); }); test('Virtual Population with justOne', async () => { const UserSchema = new localgoose.Schema({ name: String }); const PostSchema = new localgoose.Schema({ title: String, userId: String }); UserSchema.virtual('latestPost', { ref: 'ParityPost', localField: '_id', foreignField: 'userId', justOne: true }); const VUser = db.model('ParityUser', UserSchema); const VPost = db.model('ParityPost', PostSchema); const u = await VUser.create({ name: 'PopUser' }); await VPost.create({ title: 'Post 1', userId: u._id }); await VPost.create({ title: 'Post 2', userId: u._id }); const doc = await VUser.findOne({ _id: u._id }).populate('latestPost').exec(); assert(doc.latestPost && doc.latestPost.title, 'Should have populated virtual'); assert(!Array.isArray(doc.latestPost), 'justOne should return single object'); }); test('Query/Update Middleware Hooks', async () => { let preCalled = false; let postCalled = false; const schema = new localgoose.Schema({ name: String }); schema.pre('findOneAndUpdate', function() { preCalled = true; }); schema.post('findOneAndUpdate', function() { postCalled = true; }); const M = db.model('ParityHookModel', schema); await M.create({ name: 'old' }); await M.findOneAndUpdate({ name: 'old' }, { name: 'new' }); assert(preCalled); assert(postCalled); let delPreCalled = false; const schemaDel = new localgoose.Schema({ name: String }); schemaDel.pre('deleteOne', function() { delPreCalled = true; }); const M2 = db.model('ParityDeleteModel', schemaDel); await M2.create({ name: 'test' }); await M2.deleteOne({ name: 'test' }); assert(delPreCalled); }); test('Document utilities (overwrite, $getAllSubdocs)', async () => { const M = db.model('ParityUt