@small-tech/jsdb
Version:
A zero-dependency, transparent, in-memory, streaming write-on-update JavaScript database for Small Web applications that persists to a JavaScript transaction log.
198 lines (171 loc) • 7.32 kB
JavaScript
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// JSDF class.
//
// Copyright ⓒ 2020-2021 Aral Balkan. Licensed under AGPLv3 or later.
// Shared with ♥ by the Small Technology Foundation.
//
// Recursively serialises a JavaScript data structure into JavaScript Data Format (a series
// of JavaScript statements that atomically recreates said data structure when run).
//
// Like this? Fund us!
// https://small-tech.org/fund-us
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
import { quoteKeyIfNotSafeInteger, sanitisedString } from './Util.js'
export default class JSDF {
static serialise (value, key, parentType = null) {
// Check that key is valid.
if (key === undefined || key === null) {
throw new Error('Key cannot be undefined or null.')
}
const objectType = value === undefined ? 'undefined' : value === null ? 'null' : value.constructor === undefined ? 'ObjectWithNullPrototype' : value.constructor.name === '' ? 'unknown' : value.constructor.name
let serialisedValue
// Attempt to serialise if this is a supported intrinsic object
// (and throw an error if it is an unsupported intrinsic object).
switch (objectType) {
//
// Supported intrinsic objects.
//
// Simple data types that don’t need to be modified.
// Note: Number will include Infinity and NaN. This is fine.
case 'Number':
case 'Boolean':
case 'undefined':
case 'null': {
serialisedValue = value
break
}
case 'String': {
//
// Note: it is important that we sanitise string input before storing it to
// ===== thwart arbitrary code execution via injection attacks. So we:
//
// - Escape all backslashes (why? See https://source.small-tech.org/site.js/lib/jsdb/-/issues/9#note_15844)
// - Escape all backticks (why? See https://source.small-tech.org/site.js/lib/jsdb/-/issues/9#note_15848)
// - Escape all dollar signs (why? See https://source.small-tech.org/site.js/lib/jsdb/-/issues/9)
//
serialisedValue = sanitisedString(value)
break
}
case 'Object':
case 'ObjectWithNullPrototype': {
const children = []
for (let childKey in value) {
if (!childKey.startsWith('_')) {
const childValue = value[childKey]
children.push(JSDF.serialise(childValue, childKey, 'object'))
}
}
const properties = `{ ${children.join(', ')} }`
serialisedValue = objectType === 'Object' ? `${properties}` : `Object.create(null, Object.getOwnPropertyDescriptors(${properties}))`
break
}
case 'Array': {
const children = []
const numberOfChildren = value.length
for (let i = 0; i < numberOfChildren; i++) {
const childValue = value[i]
children.push(JSDF.serialise(childValue, i, 'array'))
}
serialisedValue = `[ ${children.join(', ')} ]`
break
}
case 'Date':
serialisedValue = `new Date('${value.toJSON()}')`
break
//
// Unsupported intrinsic objects.
// For reference, see:
// http://www.ecma-international.org/ecma-262/6.0/#sec-well-known-intrinsic-objects
//
// Might support in the future.
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
// Note TypedArray is not included as it is never directly exposed.
// (See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray)
case 'ArrayBuffer':
case 'Float32Array':
case 'Float64Array':
case 'Int8Array':
case 'Int16Array':
case 'Int32Array':
case 'Uint8Array':
case 'Uint16Array':
case 'Uint32Array':
case 'Uint8ClampedArray':
//
// Does not make sense to support.
//
// (Note: we do not include Proxy here since, by its very nature, it is a transparent datatype.
// We couldn’t detect it using this method anyway; we’d have to use util.types.isProxy().)
case 'DataView':
case 'Function':
case 'GeneratorFunction':
case 'Generator':
case 'Promise':
case 'RegExp':
case 'Symbol':
case 'Error':
case 'EvalError':
case 'RangeError':
case 'ReferenceError':
case 'SyntaxError':
case 'TypeError':
case 'URIError': {
throw new TypeError(`You cannot store objects of type ${objectType} in JSDB.`)
// (no break necessary after throwing an error)
}
case 'unknown': {
throw new TypeError(`Cannot store object with unknown type in JSDB (did you try to store a generator instance?)`)
// (no break necessary after throwing an error)
}
default: {
const children = []
for (let childKey in value) {
// We do not persist symbols. Also, properties that start with _ are considered
// internal/private and not persisted. (Exceptions like these added here must also
// be reflected in the setHandler method of the DataProxy class so that they apply
// to runtime value changes also.)
if (typeof childKey !== 'symbol' && !childKey.startsWith('_')) {
const childValue = value[childKey]
// Functions in custom objects (i.e., methods) are automatically ignored. Otherwise,
// attempting to persist a custom object with methods would throw.
// (Note that no such test is performed for plain objects. Plain objects are expected
// not to contain functions and JSDB will throw otherwise.)
if (typeof childValue !== 'function') {
children.push(JSDF.serialise(childValue, childKey, 'object'))
}
}
}
const properties = `{ ${children.join(', ')} }`
// For custom objects, we use the Reflection API (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect)
// to construct the object to ensure that the constructor is run. The idiom in JSDB for
// any custom classes you want to persist is to accept a parameter object and set every property
// on that object as an instance property. If your custom object extends a base class
// (e.g., EventEmitter, in order to emit events) don’t forget to call super() as the first statement
// in your constructor just as you normally would.
serialisedValue = `typeof ${objectType} === 'function' ? Reflect.construct(${objectType}, [${properties}]) : (() => { throw new Error('${objectType} class missing in JSDB.open()'); })()`
break
}
}
let serialisedStatement
switch(parentType) {
case null: {
serialisedStatement = `${key} = ${serialisedValue};\n`
break
}
case 'object': {
serialisedStatement = `${quoteKeyIfNotSafeInteger(key)}: ${serialisedValue}`
break
}
case 'array': {
serialisedStatement = serialisedValue
break
}
}
return serialisedStatement
}
}