Compare commits

..

4 Commits

Author SHA1 Message Date
d30c9619c5 v5.16.6
Some checks failed
Default (tags) / security (push) Successful in 48s
Default (tags) / test (push) Failing after 49s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 04:16:39 +00:00
7344ae2db3 fix(classes): Add Deno compatibility, prototype-safe decorators and safe collection accessor; bump a few deps 2025-11-17 04:16:39 +00:00
3b29a150a8 v5.16.5
Some checks failed
Default (tags) / security (push) Successful in 53s
Default (tags) / test (push) Failing after 7m30s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-16 18:19:57 +00:00
59186d84a9 fix(watcher): Update dependencies, tooling and watcher import; add .serena cache ignore 2025-11-16 18:19:57 +00:00
14 changed files with 10739 additions and 1734 deletions

1
.serena/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/cache

View File

@@ -22,5 +22,6 @@
}
}
}
]
],
"deno.enable": false
}

View File

@@ -1,5 +1,24 @@
# Changelog
## 2025-11-17 - 5.16.6 - fix(classes)
Add Deno compatibility, prototype-safe decorators and safe collection accessor; bump a few deps
- Add deno.json to enable experimentalDecorators and target ES2022/DOM for Deno builds.
- Introduce getCollectionSafe() on SmartDataDbDoc and use it for save/update/delete/findOne to avoid runtime errors when instance 'collection' is not present.
- Change several instance properties (globalSaveableProperties, uniqueIndexes, regularIndexes, saveableProperties) to 'declare' so decorator-set prototype properties are not shadowed (Deno compatibility).
- Enhance @Collection decorator: capture original constructor/prototype for Deno, define prototype getter for collection on decorated class, attach docCtor for searchableFields, and forward _svDbOptions to the original constructor to preserve serializer metadata.
- Improve text/search index handling by relying on docCtor.searchableFields and guarding text index creation.
- Bump dependencies/devDependencies: @push.rocks/smartmongo -> ^2.0.14, @git.zone/tsbuild -> ^2.7.1, @git.zone/tstest -> ^2.8.1.
- These are non-breaking runtime compatibility and developer-experience fixes; intended as a patch release.
## 2025-11-16 - 5.16.5 - fix(watcher)
Update dependencies, tooling and watcher import; add .serena cache ignore
- Bump runtime dependencies: @push.rocks/smartlog 3.1.8 → 3.1.10, @push.rocks/smartstring 4.0.15 → 4.1.0, @push.rocks/taskbuffer 3.1.7 → 3.4.0, @tsclass/tsclass 9.2.0 → 9.3.0, mongodb 6.18.0 → 6.20.0
- Bump devDependencies: @git.zone/tsbuild 2.6.7 → 2.6.8, @git.zone/tsrun 1.2.44 → 1.6.2, @git.zone/tstest 2.3.5 → 2.6.2
- Switch EventEmitter import to node:events in ts/classes.watcher.ts to use the namespaced Node import
- Add .serena/.gitignore to ignore /cache
## 2025-08-18 - 5.16.4 - fix(classes.doc (convertFilterForMongoDb))
Improve filter conversion: handle logical operators, merge operator objects, add nested filter tests and docs, and fix test script

11
deno.json Normal file
View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"lib": [
"ES2022",
"DOM"
]
},
"nodeModulesDir": "auto",
"version": "5.16.6"
}

8133
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartdata",
"version": "5.16.4",
"version": "5.16.6",
"private": false,
"description": "An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.",
"main": "dist_ts/index.js",
@@ -25,21 +25,21 @@
"dependencies": {
"@push.rocks/lik": "^6.2.2",
"@push.rocks/smartdelay": "^3.0.1",
"@push.rocks/smartlog": "^3.1.8",
"@push.rocks/smartmongo": "^2.0.12",
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartmongo": "^2.0.14",
"@push.rocks/smartpromise": "^4.0.2",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartstring": "^4.0.15",
"@push.rocks/smartstring": "^4.1.0",
"@push.rocks/smarttime": "^4.0.6",
"@push.rocks/smartunique": "^3.0.8",
"@push.rocks/taskbuffer": "^3.1.7",
"@tsclass/tsclass": "^9.2.0",
"mongodb": "^6.18.0"
"@push.rocks/taskbuffer": "^3.4.0",
"@tsclass/tsclass": "^9.3.0",
"mongodb": "^6.20.0"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.6.7",
"@git.zone/tsrun": "^1.2.44",
"@git.zone/tstest": "^2.3.5",
"@git.zone/tsbuild": "^2.7.1",
"@git.zone/tsrun": "^1.6.2",
"@git.zone/tstest": "^2.8.1",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.15.2"

3942
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

255
test/test.deno.ts Normal file
View File

@@ -0,0 +1,255 @@
// TODO: Decorator support during testing for bun and deno in @git.zone/tstest
import { tap, expect } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv';
import * as smartmongo from '@push.rocks/smartmongo';
import { smartunique } from '../ts/plugins.js';
import * as mongodb from 'mongodb';
const testQenv = new Qenv(process.cwd(), process.cwd() + '/.nogit/');
console.log(process.memoryUsage());
// the tested module
import * as smartdata from '../ts/index.js';
// =======================================
// Connecting to the database server
// =======================================
let smartmongoInstance: smartmongo.SmartMongo;
let testDb: smartdata.SmartdataDb;
const totalCars = 2000;
tap.test('should create a testinstance as database', async () => {
const databaseName = `test-smartdata-deno-${smartunique.shortId()}`;
testDb = new smartdata.SmartdataDb({
mongoDbUrl: await testQenv.getEnvVarOnDemand('MONGODB_URL'),
mongoDbName: databaseName,
});
await testDb.init();
});
// =======================================
// The actual tests
// =======================================
// ------
// Collections
// ------
@smartdata.Collection(() => {
return testDb;
})
class Car extends smartdata.SmartDataDbDoc<Car, Car> {
@smartdata.unI()
public index: string = smartunique.shortId();
@smartdata.svDb()
public color: string;
@smartdata.svDb()
public brand: string;
@smartdata.svDb()
public testBuffer = Buffer.from('hello');
@smartdata.svDb()
deepData = {
sodeep: 'yes',
};
constructor(colorArg: string, brandArg: string) {
super();
this.color = colorArg;
this.brand = brandArg;
}
}
tap.test('should create a new id', async () => {
const newid = await Car.getNewId();
console.log(newid);
});
tap.test('should save the car to the db', async (toolsArg) => {
const myCar = new Car('red', 'Volvo');
console.log('Car.collection.smartdataDb:', (Car.collection as any).smartdataDb?.mongoDb?.databaseName);
console.log('Car.collection.collectionName:', (Car.collection as any).collectionName);
console.log('testDb.mongoDb.databaseName:', testDb.mongoDb.databaseName);
await myCar.save();
const myCar2 = new Car('red', 'Volvo');
await myCar2.save();
let counter = 0;
const gottenCarInstance = await Car.getInstance({});
console.log(gottenCarInstance.testBuffer instanceof mongodb.Binary);
process.memoryUsage();
do {
const myCar3 = new Car('red', 'Renault');
await myCar3.save();
counter++;
if (counter % 100 === 0) {
console.log(
`Filled database with ${counter} of ${totalCars} Cars and memory usage ${
process.memoryUsage().rss / 1e6
} MB`,
);
}
} while (counter < totalCars);
console.log(process.memoryUsage());
// DEBUG: Check what's actually in the database
const savedCount = await Car.getCount({});
console.log('Total cars saved in DB:', savedCount);
const renaultCount = await Car.getCount({ brand: 'Renault' });
console.log('Renault cars in DB:', renaultCount);
// Check what's actually in the first saved car
const firstCar = await Car.getInstance({});
console.log('First car data:', JSON.stringify({
color: firstCar?.color,
brand: firstCar?.brand,
index: firstCar?.index
}));
});
tap.test('expect to get instance of Car with shallow match', async () => {
console.log('Before query - testDb.mongoDb.databaseName:', testDb.mongoDb.databaseName);
console.log('Before query - Car.collection.smartdataDb:', (Car.collection as any).smartdataDb?.mongoDb?.databaseName);
console.log('Before query - Car.collection.collectionName:', (Car.collection as any).collectionName);
const totalQueryCycles = totalCars / 2;
let counter = 0;
do {
const timeStart = Date.now();
const myCars = await Car.getInstances({
brand: 'Renault',
});
if (counter % 10 === 0) {
console.log(
`performed ${counter} of ${totalQueryCycles} total query cycles: took ${
Date.now() - timeStart
}ms to query a set of 2000 with memory footprint ${process.memoryUsage().rss / 1e6} MB`,
);
console.log('myCars.length:', myCars.length);
console.log('myCars[0]:', myCars[0]);
}
expect(myCars[0].deepData.sodeep).toEqual('yes');
expect(myCars[0].brand).toEqual('Renault');
counter++;
} while (counter < totalQueryCycles);
});
tap.test('expect to get instance of Car with deep match', async () => {
const totalQueryCycles = totalCars / 6;
let counter = 0;
do {
const timeStart = Date.now();
const myCars2 = await Car.getInstances({
deepData: {
sodeep: 'yes',
},
});
if (counter % 10 === 0) {
console.log(
`performed ${counter} of ${totalQueryCycles} total query cycles: took ${
Date.now() - timeStart
}ms to deep query a set of 2000 with memory footprint ${process.memoryUsage().rss / 1e6} MB`,
);
}
expect(myCars2[0].deepData.sodeep).toEqual('yes');
expect(myCars2[0].brand).toEqual('Volvo');
counter++;
} while (counter < totalQueryCycles);
});
tap.test('expect to get instance of Car and update it', async () => {
const myCar = await Car.getInstance<Car>({
brand: 'Volvo',
});
expect(myCar.color).toEqual('red');
myCar.color = 'blue';
await myCar.save();
});
tap.test('should be able to delete an instance of car', async () => {
const myCars = await Car.getInstances({
brand: 'Volvo',
color: 'blue',
});
console.log(myCars);
expect(myCars[0].color).toEqual('blue');
for (const myCar of myCars) {
await myCar.delete();
}
const myCar2 = await Car.getInstance<Car>({
brand: 'Volvo',
});
expect(myCar2.color).toEqual('red');
});
// tslint:disable-next-line: max-classes-per-file
@smartdata.Collection(() => {
return testDb;
})
class Truck extends smartdata.SmartDataDbDoc<Car, Car> {
@smartdata.unI()
public id: string = smartunique.shortId();
@smartdata.svDb()
public color: string;
@smartdata.svDb()
public brand: string;
constructor(colorArg: string, brandArg: string) {
super();
this.color = colorArg;
this.brand = brandArg;
}
}
tap.test('should store a new Truck', async () => {
const truck = new Truck('blue', 'MAN');
await truck.save();
const myTruck2 = await Truck.getInstance({ color: 'blue' });
expect(myTruck2.color).toEqual('blue');
myTruck2.color = 'red';
await myTruck2.save();
const myTruck3 = await Truck.getInstance({ color: 'blue' });
expect(myTruck3).toBeNull();
});
tap.test('should return a count', async () => {
const truckCount = await Truck.getCount();
expect(truckCount).toEqual(1);
});
tap.test('should use a cursor', async () => {
const cursor = await Car.getCursor({});
let counter = 0;
await cursor.forEach(async (carArg) => {
counter++;
counter % 50 === 0 ? console.log(`50 more of ${carArg.color}`) : null;
});
});
// =======================================
// close the database connection
// =======================================
tap.test('close', async () => {
if (smartmongoInstance) {
await smartmongoInstance.stopAndDumpToDir('./.nogit/dbdump/test.ts');
} else {
await testDb.mongoDb.dropDatabase();
await testDb.close();
}
setTimeout(() => process.exit(), 2000);
});
tap.start({ throwOnError: true });

View File

@@ -1,3 +1,5 @@
// TODO: Decorator support during testing for bun and deno in @git.zone/tstest
import { tap, expect } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv';
import * as smartmongo from '@push.rocks/smartmongo';

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartdata',
version: '5.16.4',
version: '5.16.6',
description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.'
}

View File

@@ -27,30 +27,51 @@ const collectionFactory = new CollectionFactory();
*/
export function Collection(dbArg: SmartdataDb | TDelayed<SmartdataDb>) {
return function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
// Capture original constructor's prototype in closure for Deno compatibility
const originalPrototype = constructor.prototype;
const originalConstructor = constructor as any;
const getCollection = () => {
if (!(dbArg instanceof SmartdataDb)) {
dbArg = dbArg();
}
const coll = collectionFactory.getCollection(constructor.name, dbArg);
// Attach document constructor for searchableFields lookup
if (!(coll as any).docCtor) {
(coll as any).docCtor = decoratedClass;
}
return coll;
};
const decoratedClass = class extends constructor {
public static className = constructor.name;
public static get collection() {
if (!(dbArg instanceof SmartdataDb)) {
dbArg = dbArg();
}
const coll = collectionFactory.getCollection(constructor.name, dbArg);
// Attach document constructor for searchableFields lookup
if (!(coll as any).docCtor) {
(coll as any).docCtor = decoratedClass;
}
return coll;
return getCollection();
}
public get collection() {
if (!(dbArg instanceof SmartdataDb)) {
dbArg = dbArg();
}
const coll = collectionFactory.getCollection(constructor.name, dbArg);
if (!(coll as any).docCtor) {
(coll as any).docCtor = decoratedClass;
}
return coll;
return getCollection();
}
};
// Ensure instance getter works in Deno by defining it on the prototype
Object.defineProperty(decoratedClass.prototype, 'collection', {
get: getCollection,
enumerable: false,
configurable: true
});
// Deno compatibility note: Property decorators set properties on the prototype.
// Since we removed instance property declarations from SmartDataDbDoc,
// the decorator-set prototype properties are now accessible without shadowing.
// No manual forwarding needed - natural prototype inheritance works!
// Point to original constructor's _svDbOptions
Object.defineProperty(decoratedClass, '_svDbOptions', {
get() { return originalConstructor._svDbOptions; },
set(value) { originalConstructor._svDbOptions = value; },
configurable: true
});
return decoratedClass;
};
}

View File

@@ -339,6 +339,13 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
public static manager;
public manager: TManager;
/**
* Helper to get collection with fallback to static for Deno compatibility
*/
private getCollectionSafe(): SmartdataCollection<any> {
return this.collection || (this.constructor as any).collection;
}
// STATIC
public static createInstanceFromMongoDbNativeDoc<T>(
this: plugins.tsclass.typeFest.Class<T>,
@@ -698,23 +705,28 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
/**
* an array of saveable properties of ALL doc
* Note: Set by decorators on prototype - NOT declared as instance property to avoid shadowing in Deno
* Declared with definite assignment assertion to satisfy TypeScript without creating instance property
*/
public globalSaveableProperties: string[];
declare globalSaveableProperties: string[];
/**
* unique indexes
* Note: Set by decorators on prototype - NOT declared as instance property to avoid shadowing in Deno
*/
public uniqueIndexes: string[];
declare uniqueIndexes: string[];
/**
* regular indexes with their options
* Note: Set by decorators on prototype - NOT declared as instance property to avoid shadowing in Deno
*/
public regularIndexes: Array<{field: string, options: IIndexOptions}> = [];
declare regularIndexes: Array<{field: string, options: IIndexOptions}>;
/**
* an array of saveable properties of a specific doc
* Note: Set by decorators on prototype - NOT declared as instance property to avoid shadowing in Deno
*/
public saveableProperties: string[];
declare saveableProperties: string[];
/**
* name
@@ -747,10 +759,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
// perform insert or update
switch (this.creationStatus) {
case 'db':
dbResult = await this.collection.update(self, { session: opts?.session });
dbResult = await this.getCollectionSafe().update(self, { session: opts?.session });
break;
case 'new':
dbResult = await this.collection.insert(self, { session: opts?.session });
dbResult = await this.getCollectionSafe().insert(self, { session: opts?.session });
this.creationStatus = 'db';
break;
default:
@@ -772,7 +784,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
await (this as any).beforeDelete();
}
// perform deletion
const result = await this.collection.delete(this, { session: opts?.session });
const result = await this.getCollectionSafe().delete(this, { session: opts?.session });
// allow hook after delete
if (typeof (this as any).afterDelete === 'function') {
await (this as any).afterDelete();
@@ -802,7 +814,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
* updates an object from db
*/
public async updateFromDb(): Promise<boolean> {
const mongoDbNativeDoc = await this.collection.findOne(await this.createIdentifiableObject());
const mongoDbNativeDoc = await this.getCollectionSafe().findOne(await this.createIdentifiableObject());
if (!mongoDbNativeDoc) {
return false; // Document not found in database
}

View File

@@ -1,6 +1,6 @@
import { SmartDataDbDoc } from './classes.doc.js';
import * as plugins from './plugins.js';
import { EventEmitter } from 'events';
import { EventEmitter } from 'node:events';
/**
* a wrapper for the native mongodb cursor. Exposes better