Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ import { JsonDB, Config } from 'node-json-db';
// The third argument is used to ask JsonDB to save the database in a human readable format. (default false)
// The fourth argument is the separator. By default it's slash (/)
// The fifth argument enables sync writes (default false)
// The sixth argument controls automatic Date parsing for ISO strings (default true)
var db = new JsonDB(new Config("myDataBase", true, false, '/', false, false));
var db = new JsonDB(new Config("myDataBase", true, false, '/', false));

// Pushing the data into the database
// With the wanted DataPath
Expand Down Expand Up @@ -368,6 +367,71 @@ await db.push("/test1","super test");
- The encryption uses AES-256-GCM for secure authenticated encryption
- The encryption key must be exactly 32 bytes

#### Type Serialization

JsonDB automatically serializes and deserializes JavaScript types that are not natively supported by JSON. The following types are supported out of the box:

| Type | Serialized format |
|----------|-----------------------------------------------------------------------|
| `Date` | `{ "__type": "Date", "__value": "2023-01-01T00:00:00.000Z" }` |
| `Set` | `{ "__type": "Set", "__value": [1, 2, 3] }` |
| `Map` | `{ "__type": "Map", "__value": [["key", "value"]] }` |
| `RegExp` | `{ "__type": "RegExp", "__value": { "source": "^test$", "flags": "i" } }` |
Comment thread
Belphemur marked this conversation as resolved.
| `BigInt` | `{ "__type": "BigInt", "__value": "9007199254740993" }` |

```typescript
import { JsonDB, Config } from 'node-json-db';

const db = new JsonDB(new Config('myDataBase'));

// All these types are automatically serialized and deserialized
await db.push('/data', {
tags: new Set(['typescript', 'json']),
metadata: new Map([['version', 1], ['author', 'test']]),
createdAt: new Date(),
pattern: /^hello\s+world$/i,
bigNumber: BigInt('9007199254740993'),
});

const data = await db.getData('/data');
// data.tags is a Set, data.metadata is a Map, data.createdAt is a Date, etc.
```

#### Custom Serializers

You can add support for additional types by implementing the `ISerializer` interface and registering them with `Config.addSerializer()`:

```typescript
import { JsonDB, Config, ISerializer } from 'node-json-db';

const urlSerializer: ISerializer = {
type: "URL",
serialize: (value: URL) => value.href,
deserialize: (value: string) => new URL(value),
test: (value: any) => value instanceof URL,
};

const config = new Config('myDataBase');
config.addSerializer(urlSerializer);

const db = new JsonDB(config);
await db.push('/link', new URL('https://example.com'));
const link = await db.getData('/link'); // URL instance
```

You can also use `defaultSerializers` directly with `JsonAdapter` for full control:

```typescript
import { JsonAdapter, FileAdapter, defaultSerializers, ISerializer } from 'node-json-db';

const mySerializer: ISerializer = { /* ... */ };
const adapter = new JsonAdapter(
new FileAdapter('mydb.json', false),
false,
[...defaultSerializers, mySerializer]
);
```

### Exception/Error
#### Type

Expand Down
2 changes: 2 additions & 0 deletions src/JsonDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {readLockAsync, writeLockAsync} from "./lock/Lock";
export {Config, ConfigWithAdapter} from './lib/JsonDBConfig'
export {DatabaseError, DataError} from './lib/Errors'
export type {IAdapter} from './adapter/IAdapter'
export type {ISerializer} from './adapter/data/ISerializer'
export {JsonAdapter} from './adapter/data/JsonAdapter'
export {FileAdapter} from './adapter/file/FileAdapter'
export {DateSerializer, SetSerializer, MapSerializer, RegExpSerializer, BigIntSerializer, defaultSerializers} from './adapter/data/Serializers'
Comment thread
Belphemur marked this conversation as resolved.

type DataPath = Array<string>

Expand Down
39 changes: 39 additions & 0 deletions src/adapter/data/ISerializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Contract for custom type serialization/deserialization.
* Implement this interface to add support for custom types
* that are not natively supported by JSON.
*
* Types are serialized using a `__type` discriminator:
* ```json
* { "__type": "TypeName", "__value": <serialized-data> }
* ```
*/
export interface ISerializer {
/**
* Unique type identifier stored as `__type` in the serialized JSON.
*/
readonly type: string;

/**
* Serialize a value to a JSON-compatible representation.
* The returned value will be stored under `__value` in the serialized JSON.
* @param value The value to serialize
* @returns A JSON-compatible value
*/
serialize(value: any): any;

/**
* Deserialize a value from its JSON representation.
* The input is the `__value` from the serialized JSON.
* @param value The stored JSON value
* @returns The deserialized runtime value
*/
deserialize(value: any): any;

/**
* Test if a runtime value should be handled by this serializer.
* @param value The value to test
* @returns true if this serializer should handle this value
*/
test(value: any): boolean;
}
63 changes: 45 additions & 18 deletions src/adapter/data/JsonAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import {IAdapter} from "../IAdapter";
import {ISerializer} from "./ISerializer";
import {defaultSerializers} from "./Serializers";

export class JsonAdapter implements IAdapter<any> {

private readonly adapter: IAdapter<string>;
private readonly humanReadable: boolean;
private readonly dateRegex = new RegExp('^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}', 'm')
private readonly parseDates: boolean;
private readonly serializers: readonly ISerializer[];


constructor(adapter: IAdapter<string>, humanReadable: boolean = false, parseDates: boolean = true) {
/**
* @param adapter The underlying string adapter for reading/writing raw data
* @param humanReadable Whether to pretty-print the JSON output
* @param serializers Custom serializers for complex types (default: Date, Set, Map, RegExp, BigInt).
*/
constructor(adapter: IAdapter<string>, humanReadable: boolean = false, serializers: readonly ISerializer[] = defaultSerializers) {
this.adapter = adapter;
this.humanReadable = humanReadable;
this.parseDates = parseDates;
}

private replacer(key: string, value: any): any {
return value;
}

private reviver(key: string, value: any): any {
if (this.parseDates && typeof value == "string" && this.dateRegex.test(value)) {
return new Date(value);
}
return value;
this.serializers = serializers;
}

async readAsync(): Promise<any> {
Expand All @@ -31,15 +26,47 @@ export class JsonAdapter implements IAdapter<any> {
await this.writeAsync({});
return {};
}
return JSON.parse(data, this.reviver.bind(this));
const serializers = this.serializers;
const reviver = function (key: string, value: any): any {
if (value !== null && typeof value === 'object' && '__type' in value) {
// Un-escape user data that naturally contained __type
if (typeof value.__type === 'object' && value.__type !== null && '__escaped' in value.__type) {
return {...value, __type: value.__type.__escaped};
}
// Deserialize known serialized types
if ('__value' in value) {
for (const serializer of serializers) {
if (value.__type === serializer.type) {
return serializer.deserialize(value.__value);
}
}
}
}
return value;
};
return JSON.parse(data, reviver);
}

writeAsync(data: any): Promise<void> {
const serializers = this.serializers;
const replacer = function (this: any, key: string, value: any): any {
const rawValue = this[key];
for (const serializer of serializers) {
if (rawValue !== null && rawValue !== undefined && serializer.test(rawValue)) {
return {__type: serializer.type, __value: serializer.serialize(rawValue)};
}
}
// Escape plain objects that naturally contain __type to prevent false deserialization
if (typeof value === 'object' && value !== null && !Array.isArray(value) && '__type' in value) {
return {...value, __type: {__escaped: value.__type}};
}
return value;
};
let stringify = '';
if (this.humanReadable) {
stringify = JSON.stringify(data, this.replacer.bind(this), 4)
stringify = JSON.stringify(data, replacer, 4)
} else {
stringify = JSON.stringify(data, this.replacer.bind(this))
stringify = JSON.stringify(data, replacer)
}
return this.adapter.writeAsync(stringify);
}
Expand Down
154 changes: 154 additions & 0 deletions src/adapter/data/Serializers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {ISerializer} from "./ISerializer";

/**
* Serializer for JavaScript Date objects.
* Serializes a Date as an ISO 8601 string.
*
* @example
* ```json
* { "__type": "Date", "__value": "2023-01-01T00:00:00.000Z" }
* ```
*/
export class DateSerializer implements ISerializer {
readonly type = "Date";

serialize(value: Date): string {
return value.toISOString();
}

deserialize(value: string): Date {
return new Date(value);
}

test(value: any): boolean {
return value instanceof Date;
}
}

/**
* Serializer for JavaScript Set objects.
* Serializes a Set as an array of its values.
*
* @example
* ```json
* { "__type": "Set", "__value": [1, 2, 3] }
* ```
*/
export class SetSerializer implements ISerializer {
readonly type = "Set";

serialize(value: Set<any>): any[] {
return [...value];
}

deserialize(value: any[]): Set<any> {
return new Set(value);
}

test(value: any): boolean {
return value instanceof Set;
}
}

/**
* Serializer for JavaScript Map objects.
* Serializes a Map as an array of key-value pairs.
*
* @example
* ```json
* { "__type": "Map", "__value": [["key1", "value1"], ["key2", "value2"]] }
* ```
*/
export class MapSerializer implements ISerializer {
readonly type = "Map";

serialize(value: Map<any, any>): [any, any][] {
return [...value];
}

deserialize(value: [any, any][]): Map<any, any> {
return new Map(value);
}

test(value: any): boolean {
return value instanceof Map;
}
}

/**
* Serializer for JavaScript RegExp objects.
* Serializes a RegExp as an object with source and flags.
*
* @example
* ```json
* { "__type": "RegExp", "__value": { "source": "hello\\s+world", "flags": "gi" } }
* ```
*/
export class RegExpSerializer implements ISerializer {
readonly type = "RegExp";

serialize(value: RegExp): { source: string; flags: string } {
return {source: value.source, flags: value.flags};
}

deserialize(value: { source: string; flags: string }): RegExp {
return new RegExp(value.source, value.flags);
}

test(value: any): boolean {
return value instanceof RegExp;
}
}

/**
* Serializer for JavaScript BigInt values.
* Serializes a BigInt as a string representation.
*
* @example
* ```json
* { "__type": "BigInt", "__value": "9007199254740993" }
* ```
*/
export class BigIntSerializer implements ISerializer {
readonly type = "BigInt";

serialize(value: bigint): string {
return value.toString();
}

deserialize(value: string): bigint {
return BigInt(value);
}

test(value: any): boolean {
return typeof value === 'bigint';
}
}

/**
* Default serializers included with the JsonAdapter.
* Provides built-in support for Date, Set, Map, RegExp, and BigInt types.
*
* This array is frozen (immutable). To extend it with your own serializers,
* spread it into a new array:
* ```typescript
* import { defaultSerializers, ISerializer } from 'node-json-db';
*
* const mySerializer: ISerializer = {
* type: "MyType",
* serialize: (value) => value.toJSON(),
* deserialize: (value) => MyType.fromJSON(value),
* test: (value) => value instanceof MyType,
* };
*
* // Use with Config.addSerializer() or spread into a custom list:
* const serializers = [...defaultSerializers, mySerializer];
* ```
*/
export const defaultSerializers: readonly ISerializer[] = Object.freeze([
new DateSerializer(),
new SetSerializer(),
new MapSerializer(),
new RegExpSerializer(),
new BigIntSerializer(),
]);
Loading