Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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;
}
59 changes: 42 additions & 17 deletions src/adapter/data/JsonAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
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: 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 parseDates Whether to serialize/deserialize Date objects (default: true).
* When true, a DateSerializer is included in the serializer list.
* When false, Date objects are excluded from serialization.
* @param serializers Custom serializers for complex types (default: Date, Set, Map, RegExp, BigInt).
* When providing custom serializers, the parseDates flag still controls
* whether the DateSerializer is included.
*/
constructor(adapter: IAdapter<string>, humanReadable: boolean = false, parseDates: boolean = true, serializers: 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);
if (parseDates) {
this.serializers = serializers;
} else {
this.serializers = serializers.filter(s => s.type !== "Date");
}
Comment thread
Belphemur marked this conversation as resolved.
Outdated
return value;
}

async readAsync(): Promise<any> {
Expand All @@ -31,15 +35,36 @@ 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 && '__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)};
}
}
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
152 changes: 152 additions & 0 deletions src/adapter/data/Serializers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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.
*
* You can extend this list with your own serializers:
* ```typescript
* import { JsonAdapter, 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,
* };
*
* const adapter = new JsonAdapter(fileAdapter, false, true, [...defaultSerializers, mySerializer]);
* ```
*/
export const defaultSerializers: ISerializer[] = [
new DateSerializer(),
new SetSerializer(),
new MapSerializer(),
new RegExpSerializer(),
new BigIntSerializer(),
];
Comment thread
Belphemur marked this conversation as resolved.
Outdated
Loading