Compare commits

..

6 Commits

Author SHA1 Message Date
d254f58a05 v5.16.7
Some checks failed
Default (tags) / security (push) Successful in 47s
Default (tags) / test (push) Failing after 49s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 08:05:28 +00:00
c5e7b6f982 fix(classes.collection): Improve Deno and TypeScript compatibility: Collection decorator _svDbOptions forwarding and config cleanup 2025-11-17 08:05:28 +00:00
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
16 changed files with 10744 additions and 1736 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,31 @@
# Changelog # Changelog
## 2025-11-17 - 5.16.7 - fix(classes.collection)
Improve Deno and TypeScript compatibility: Collection decorator _svDbOptions forwarding and config cleanup
- Collection decorator: capture original constructor and forward _svDbOptions to ensure property decorator options (serialize/deserialize) remain accessible in Deno environments.
- Collection decorator: keep instance getter defined on prototype for Deno compatibility (no behavior change, clarifies forwarding logic).
- Build/config: removed experimentalDecorators and useDefineForClassFields from deno.json and tsconfig.json to avoid Deno/TS build issues and rely on default compilation settings.
## 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)) ## 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 Improve filter conversion: handle logical operators, merge operator objects, add nested filter tests and docs, and fix test script

10
deno.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"lib": [
"ES2022",
"DOM"
]
},
"nodeModulesDir": "auto",
"version": "5.16.7"
}

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", "name": "@push.rocks/smartdata",
"version": "5.16.4", "version": "5.16.7",
"private": false, "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.", "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", "main": "dist_ts/index.js",
@@ -25,21 +25,21 @@
"dependencies": { "dependencies": {
"@push.rocks/lik": "^6.2.2", "@push.rocks/lik": "^6.2.2",
"@push.rocks/smartdelay": "^3.0.1", "@push.rocks/smartdelay": "^3.0.1",
"@push.rocks/smartlog": "^3.1.8", "@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartmongo": "^2.0.12", "@push.rocks/smartmongo": "^2.0.14",
"@push.rocks/smartpromise": "^4.0.2", "@push.rocks/smartpromise": "^4.0.2",
"@push.rocks/smartrx": "^3.0.10", "@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/smarttime": "^4.0.6",
"@push.rocks/smartunique": "^3.0.8", "@push.rocks/smartunique": "^3.0.8",
"@push.rocks/taskbuffer": "^3.1.7", "@push.rocks/taskbuffer": "^3.4.0",
"@tsclass/tsclass": "^9.2.0", "@tsclass/tsclass": "^9.3.0",
"mongodb": "^6.18.0" "mongodb": "^6.20.0"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.6.7", "@git.zone/tsbuild": "^2.7.1",
"@git.zone/tsrun": "^1.2.44", "@git.zone/tsrun": "^1.6.2",
"@git.zone/tstest": "^2.3.5", "@git.zone/tstest": "^2.8.1",
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/tapbundle": "^6.0.3", "@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.15.2" "@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 { tap, expect } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv'; import { Qenv } from '@push.rocks/qenv';
import * as smartmongo from '@push.rocks/smartmongo'; import * as smartmongo from '@push.rocks/smartmongo';

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartdata', name: '@push.rocks/smartdata',
version: '5.16.4', version: '5.16.7',
description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.' 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,50 @@ const collectionFactory = new CollectionFactory();
*/ */
export function Collection(dbArg: SmartdataDb | TDelayed<SmartdataDb>) { export function Collection(dbArg: SmartdataDb | TDelayed<SmartdataDb>) {
return function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) { return function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
// Capture original constructor for _svDbOptions forwarding
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 { const decoratedClass = class extends constructor {
public static className = constructor.name; public static className = constructor.name;
public static get collection() { public static get collection() {
if (!(dbArg instanceof SmartdataDb)) { return getCollection();
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;
} }
public get collection() { public get collection() {
if (!(dbArg instanceof SmartdataDb)) { return getCollection();
dbArg = dbArg();
}
const coll = collectionFactory.getCollection(constructor.name, dbArg);
if (!(coll as any).docCtor) {
(coll as any).docCtor = decoratedClass;
}
return coll;
} }
}; };
// 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; return decoratedClass;
}; };
} }

View File

@@ -339,6 +339,13 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
public static manager; public static manager;
public manager: TManager; 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 // STATIC
public static createInstanceFromMongoDbNativeDoc<T>( public static createInstanceFromMongoDbNativeDoc<T>(
this: plugins.tsclass.typeFest.Class<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 * 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 * 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 * 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 * 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 * name
@@ -747,10 +759,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
// perform insert or update // perform insert or update
switch (this.creationStatus) { switch (this.creationStatus) {
case 'db': case 'db':
dbResult = await this.collection.update(self, { session: opts?.session }); dbResult = await this.getCollectionSafe().update(self, { session: opts?.session });
break; break;
case 'new': case 'new':
dbResult = await this.collection.insert(self, { session: opts?.session }); dbResult = await this.getCollectionSafe().insert(self, { session: opts?.session });
this.creationStatus = 'db'; this.creationStatus = 'db';
break; break;
default: default:
@@ -772,7 +784,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
await (this as any).beforeDelete(); await (this as any).beforeDelete();
} }
// perform deletion // 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 // allow hook after delete
if (typeof (this as any).afterDelete === 'function') { if (typeof (this as any).afterDelete === 'function') {
await (this as any).afterDelete(); await (this as any).afterDelete();
@@ -802,7 +814,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
* updates an object from db * updates an object from db
*/ */
public async updateFromDb(): Promise<boolean> { 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) { if (!mongoDbNativeDoc) {
return false; // Document not found in database return false; // Document not found in database
} }

View File

@@ -1,6 +1,6 @@
import { SmartDataDbDoc } from './classes.doc.js'; import { SmartDataDbDoc } from './classes.doc.js';
import * as plugins from './plugins.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 * a wrapper for the native mongodb cursor. Exposes better

View File

@@ -1,7 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",