/**
* @author Michael Hasenstein <hasenstein@yahoo.com>
* @copyright REFINIO GmbH 2017
* @license CC-BY-NC-SA-2.5; portions MIT License
* @version 0.0.1
*/
/**
* @private
* @module
*/
/**
* The type returned by *write* operation(s) for *versioned* ONE objects as well as by
* {@link getObjectByIdHash} and {@link getObjectByIdObject}. This is similar to
* {@link UnversionedObjectResult} except that it also includes the `idHash` property which
* identifies all versions of a versioned object as well as the timestamp written into the
* version map.
*
* @global
* @typedef {object} VersionedObjectResult
* @property {OneVersionedObjectTypes} obj - The versioned ONE object that was stored. This is only
* set for ONE objects representable as Javascript objects.
* @property {SHA256Hash} hash - The crypto hash of the microdata representation of the object
* and also the filename.
* @property {SHA256IdHash} idHash - The crypto hash of the ID-object of the ONE object.
* @property {number} [timestamp] - Creation date-time of the version-map entry for the object.
* This is not the creation date-time of the object! The timestamp is only provided if a version
* map entry was made. Version map entries can be made repeatedly for already existing objects
* to make an already existing object the most current version without creating it (since it
* already exists...)
* @property {FileCreationStatus} status - A string constant showing whether the file
* already existed or if it had to be created or if a new version was added.
*/
export interface VersionedObjectResult<
T extends OneVersionedObjectTypes = OneVersionedObjectTypes
> {
readonly obj: T;
hash: SHA256Hash<T>;
idHash: SHA256IdHash<T>;
status: FileCreationStatus;
timestamp: undefined | number;
}
/**
* Result value for storing versioned objects without altering the version map.
*/
export interface VersionedObjectOmitVersionMapResult<
T extends OneVersionedObjectTypes = OneVersionedObjectTypes
> {
readonly obj: T;
hash: SHA256Hash<T>;
idHash?: void;
status: FileCreationStatus;
timestamp?: void;
}
/**
* Return result object of creating ID objects with function
* `storeIdObject(obj: OneVersionedObjectTypes | OneIdObjectTypes)`.
* @global
* @typedef {object} IdFileCreation
* @property {SHA256IdHash} idHash - The SHA-256 hash of the ID ONE object
* @property {FileCreationStatus} status - A string constant showing whether the file
* already existed or if it had to be created.
*/
export interface IdFileCreation<
T extends OneVersionedObjectTypes | OneIdObjectTypes =
| OneVersionedObjectTypes
| OneIdObjectTypes
> {
idHash: SHA256IdHash<OneVersionedObjectInterfaces[T['$type$']]>;
status: FileCreationStatus;
}
import type {OneIdObjectInterfaces, OneVersionedObjectInterfaces} from '@OneObjectInterfaces';
import {createError} from './errors';
import {extractIdObject} from './microdata-to-id-hash';
import {convertIdMicrodataToObject} from './microdata-to-object';
import {isVersionedObject} from './object-recipes';
import {convertObjToIdMicrodata, convertObjToMicrodata} from './object-to-microdata';
import type {
OneIdObjectTypes,
OneVersionedObjectTypeNames,
OneVersionedObjectTypes
} from './recipes';
import {reverseMapUpdater, reverseMapUpdaterForIdObject} from './reverse-map-updater';
import type {FileCreationStatus} from './storage-base-common';
import {CREATION_STATUS} from './storage-base-common';
import {storeUTF8Clob} from './storage-blob';
import {setIdHash} from './storage-id-hash-cache';
import {getObjectWithType} from './storage-unversioned-objects';
import {createCryptoHash} from './system/crypto-helpers';
import {readUTF8TextFile, writeUTF8TextFile} from './system/storage-base';
import {clone} from './util/clone-object';
import {calculateIdHashOfObj} from './util/object';
import type {OneEventSource, OneEventSourceConsumer} from './util/one-event-source';
import {createEventSource} from './util/one-event-source';
import {serializeWithType} from './util/promise';
import type {SHA256Hash, SHA256IdHash} from './util/type-checks';
import {isObject} from './util/type-checks-basic';
import {getNthVersionMapEntry} from './version-map-query';
import type {VersionUpdatePolicyValues} from './version-map-updater';
import {createVersionMapOrAppendEntry, VERSION_UPDATES} from './version-map-updater';
/**
* Exported for plan-existing-result.js to be able to send events about previously created
* objects. Depending on the version map update policy those may still lead to actual storage
* state changes, and depending on the application's use of those events even no storage state
* change, just the fact that the object was to be created (even if it already exists), may lead
* to state application changes.
* @private
* @type {EventSource<VersionedObjectResult>}
*/
export const versionedObjEvent: OneEventSource<VersionedObjectResult> = createEventSource();
/**
* @static
* @type {EventSourceConsumer<VersionedObjectResult>}
*/
export const onVersionedObj: OneEventSourceConsumer<VersionedObjectResult> =
versionedObjEvent.consumer;
/**
* This event triggers on newly stored id-objects.
* @type {OneEventSource<VersionedObjectResult<OneVersionedObjectTypes>>}
*/
export const idObjEvent: OneEventSource<IdFileCreation> = createEventSource();
/**
* @static
* @type {OneEventSourceConsumer<VersionedObjectResult>}
*/
export const onIdObj: OneEventSourceConsumer<IdFileCreation> = idObjEvent.consumer;
/**
* @static
* @async
* @param {OneIdObjectTypes} obj - An ID object of a versioned object type, i.e. it has only
* ID properties (`isId` is `true` for the property in the recipe)
* @returns {Promise<IdFileCreation>} Returns an {@link IdFileCreation} object
*/
export async function storeIdObject<T extends OneVersionedObjectTypes | OneIdObjectTypes>(
obj: T
): Promise<IdFileCreation<T>> {
const idObjMicrodata = convertObjToIdMicrodata(obj);
const idHash = (await createCryptoHash(idObjMicrodata)) as unknown as SHA256IdHash<
OneVersionedObjectInterfaces[T['$type$']]
>;
const result = {idHash, status: await writeUTF8TextFile(idObjMicrodata, idHash)};
await reverseMapUpdaterForIdObject(obj, result);
idObjEvent.dispatch(result);
return result;
}
/**
* Returns the ID object for the given ID hash. Those are stored under the idHash as filename
* in the 'objects' storage space. There can be "empty" versioned objects for which only an ID
* object exists but no concrete versions yet, so that it is possible to ID-hash-link to ID
* objects without existing versions and still be able to find out the concrete ID properties
* which lead to the given ID hash.
* @static
* @async
* @param {SHA256IdHash} idHash - Hash of a ONE ID object
* @returns {Promise<OneIdObjectTypes>} The promise is rejected with an Error whose name
* property is set to "FileNotFoundError" if the object does not exist.
*/
export async function getIdObject<T extends OneVersionedObjectTypes | OneIdObjectTypes>(
idHash: SHA256IdHash<T>
): Promise<OneIdObjectInterfaces[T['$type$']]> {
const idObjMicrodata = await readUTF8TextFile(idHash);
return convertIdMicrodataToObject<T['$type$']>(idObjMicrodata);
}
/**
* Object writing policy:
* a) If the exact same object already exists do nothing, just return the hash of the object.
* b) If a version of the object exists (based on ID-object) but not exactly the same, create
* a new Object. Update the version map.
* c) If there is no such object yet (based on ID-object), create a new object and its
* accompanying Object-map.
* In any case, always return both hashes for the object and the id-object.
*
* 1 Calculate object-ID (hash) from the object (obj => objID)
* The object's id hash also is the name of the version map hash
* 2 Store object (obj => hash)
* 3 Attempt to update the version map. Its filename is fixed based on the empty version map and
* the
* respective ID-object hash. If this succeeds we know the map and therefore previous
* version(s) of the object exist. The promise rejects when the version map does not exist.
* IF version map does NOT exist:
* 4 Create a new version map.
* END
*
* @static
* @async
* @param {OneVersionedObjectTypes} obj - A versioned ONE object **which is cloned** to not be
* affected if the calling code mutates the object later. This is especially important because
* the (cloned) object (reference) is included in the result returned by this function. Cloning
* ensures the hash data matches the object even if the original object is mutated.
* @param {VersionUpdatePolicyValues} [versionMapUpdatePolicy=1]
* @returns {Promise<VersionedObjectResult>} Resolves with the crypto hashes of the object
* and of the accompanying (virtual) ID- and (real) object. The "status" is CREATION_STATUS.NEW
* if this particular object is new, CREATION_STATUS.EXISTS if it already exists.
*/
export async function storeVersionedObject<T extends OneVersionedObjectTypes>(
obj: T,
versionMapUpdatePolicy: VersionUpdatePolicyValues = VERSION_UPDATES.NONE_IF_LATEST
): Promise<VersionedObjectResult<T>> {
if (!isObject(obj)) {
throw createError('SVO-SO1', {type: obj === null ? 'null' : typeof obj, obj});
}
if (!isVersionedObject(obj)) {
throw createError('SVO-SO2', {obj});
}
// Trading (mostly memory-) efficiency for correctness: Insurance against mutation
// TODO Get rid of "obj" in the creation result object so that we can stop worrying about
// mutability of the object to which we only got a pointer
const clonedObj = clone(obj);
const objMicrodata = convertObjToMicrodata(clonedObj);
// This is the ID hash of the object, not the hash of the object. The version map that
// stores the hashes of all versions of this object is stored using this hash as map name.
const objId = (await createCryptoHash(
// This is a string: We know we have a versioned object, so we will not get undefined
extractIdObject(objMicrodata) as string
)) as unknown as SHA256IdHash<T>;
async function serializedStore(): Promise<VersionedObjectResult<T>> {
// THE MAIN STEP. If this object already exists the promise will be *rejected* with an
// error, so subsequent "then()" steps chained to this one below are not executed. We don't
// check for existence of the object because we subscribe to the node.js philosophy of not
// adding a useless I/O call when we get the exact same information when we try to write to
// it anyway. We don't tell storeUTF8Clob() to ignore EEXIST errors because when an object
// already exists we don't want to update the version map, but we simply do nothing.
const objFileWriteResult = await storeUTF8Clob<T>(objMicrodata);
// Optional, an optimization. If we don't put it into the cache, and it is queried, it would
// have to be loaded from storage and calculated. We assume that there is a significant
// probability that when we store a versioned object its ID hash will be needed very soon,
// for example to write reverse maps when shortly hereafter another object is written
// referencing this one.
setIdHash(objFileWriteResult.hash, objId);
// The reverse and version map updates are independent and can be run in parallel.
const [, {status, timestamp}] = await Promise.all([
reverseMapUpdater(clonedObj, objFileWriteResult),
createVersionMapOrAppendEntry(
objId, // map filename
objFileWriteResult.hash, // value
objFileWriteResult.status, // 'NEW' or 'EXISTS'
versionMapUpdatePolicy
)
]);
if (status === CREATION_STATUS.NEW) {
await storeIdObject(obj);
}
const objCreationResult: VersionedObjectResult<T> = {
obj: clonedObj,
hash: objFileWriteResult.hash,
idHash: objId,
status: objFileWriteResult.status,
timestamp
};
versionedObjEvent.dispatch(objCreationResult);
return objCreationResult;
}
return serializeWithType(`ID ${objId}`, serializedStore);
}
/**
* Stores the passed versioned object without appending it to the version map.
*
* @param {T} obj
* @returns {Promise<VersionedObjectOmitVersionMapResult<T>>}
*/
export async function storeVersionedObjectOmitVersionMap<T extends OneVersionedObjectTypes>(
obj: T
): Promise<VersionedObjectOmitVersionMapResult<T>> {
if (!isObject(obj)) {
throw createError('SVOOV-SO1', {type: obj === null ? 'null' : typeof obj, obj});
}
if (!isVersionedObject(obj)) {
throw createError('SVOOV-SO2', {obj});
}
const clonedObj = clone(obj);
const objMicrodata = convertObjToMicrodata(clonedObj);
const objFileWriteResult = await storeUTF8Clob<T>(objMicrodata);
if (objFileWriteResult.status === CREATION_STATUS.NEW) {
await reverseMapUpdater(clonedObj, objFileWriteResult);
// This writes the id object more often than necessary, because the version map / id-object
// might already exist. Better would be checking for the existence of the version map, but
// there is no call at the moment that does this.
await storeIdObject(obj);
}
return {
obj: clonedObj,
hash: objFileWriteResult.hash,
status: objFileWriteResult.status
};
}
// OVERLOADED DEFINITIONS for this function's different parameter options
export function getObjectByIdHash<T extends OneVersionedObjectTypes>(
idHash: SHA256IdHash<T>
): Promise<VersionedObjectResult<T>>;
export function getObjectByIdHash<T extends OneVersionedObjectTypes>(
idHash: SHA256IdHash<T>,
type: '*',
index?: number
): Promise<VersionedObjectResult<T>>;
export function getObjectByIdHash<T extends OneVersionedObjectTypeNames>(
idHash: SHA256IdHash<OneVersionedObjectInterfaces[T]>,
type: T,
index?: number
): Promise<VersionedObjectResult<OneVersionedObjectInterfaces[T]>>;
export function getObjectByIdHash<T extends OneVersionedObjectTypeNames>(
idHash: SHA256IdHash<OneVersionedObjectInterfaces[T]>,
type: T[],
index?: number
): Promise<VersionedObjectResult<OneVersionedObjectInterfaces[T]>>;
/**
* Returns the latest version of the object specified by the ID hash or the version specified by
* the optional index parameter, or a rejected promise if there is no such object.
* @static
* @async
* @param {SHA256IdHash} idHash - Hash of a ONE ID object
* @param {OneVersionedObjectTypeNames|OneVersionedObjectTypeNames[]} [type='*'] - Any one of the
* type string constant from ONE object recipes for a versioned object. If a type name or an
* array of type names are provided the object must be of this type or an `Error` will be
* thrown, if the parameter is left undefined or set to '*' (the default) no type check will be
* performed.
* @param {number} [index=-1] - An optional zero-based integer index (0, 1,...) in the version map
* from which the object should be returned. If the index is *negative* the count starts at
* the end of the list of versions. For example, to get the latest entry you can use an index of
* -1, -2 for the last but one, etc. If no index is provided the latest version will be returned,
* equivalent to an index of -1.
* @returns {Promise<VersionedObjectResult<T>>} The promise is rejected with an Error whose name
* property is set to "FileNotFoundError" if the object does not exist, or with an Error if it
* is of the wrong type.
*/
export async function getObjectByIdHash(
idHash: SHA256IdHash,
type: any | any[] = '*',
index: number = -1
): Promise<VersionedObjectResult> {
const {timestamp, hash} = await getNthVersionMapEntry(idHash, index);
// When checking the type TypeScript says obj is "any", but it really does find the right
// type when this function is actually called, using the overloaded definitions for this
// function.
const obj = (await getObjectWithType(hash, type)) as OneVersionedObjectTypes;
return {
obj,
hash,
idHash,
status: CREATION_STATUS.EXISTS,
timestamp
};
}
// // OVERLOADED DEFINITIONS for this function's different parameter options
// export async function getObjectByIdObj<T extends OneVersionedObjectTypes>(
// obj: T,
// index?: number
// ): Promise<VersionedObjectResult<OneVersionedObjectInterfaces[T['$type$']]>>;
// export async function getObjectByIdObj<T extends OneIdObjectTypes>(
// obj: T,
// index?: number
// ): Promise<VersionedObjectResult<OneVersionedObjectInterfaces[T['$type$']]>>;
/**
* Frontend for getObjectByIdHash() that accepts an ID object and calculates its crypto hash
* before calling that function. Returns the latest version of the object specified by the
* ID hash, or a rejected promise if there is no such object. A type string parameter like in
* function {@link storage-versioned-objects.module:ts.getObjectByIdHash|getObjectByIdHash} is not
* needed because we can use the `type` property of the given object.
* @static
* @async
* @param {(OneVersionedObjectTypes|OneIdObjectTypes)} obj - A versioned ONE object of type
* `<T>` which is used to create its ID hash. Note that it does not have to be an ID object,
* i.e. it can have more properties than just ID properties, they will be ignored.
* @param {number} [index=-1] - An optional zero-based integer index (0, 1,...) in the version map
* from which the object should be returned. If the index is *negative* the count starts at
* the end of the list of versions. For example, to get the latest entry you can use an index of
* -1, -2 for the last but one, etc. If no index is provided the latest version will be returned,
* equivalent to an index of -1.
* @returns {Promise<VersionedObjectResult<T>>} The promise is rejected with an Error whose name
* property is set to "FileNotFoundError" if the object does not exist, or with an Error if it
* is of the wrong type.
*/
export async function getObjectByIdObj<T extends OneVersionedObjectTypes | OneIdObjectTypes>(
obj: T,
index: number = -1
): Promise<VersionedObjectResult<OneVersionedObjectInterfaces[T['$type$']]>> {
const idHash = await calculateIdHashOfObj<T>(obj);
return (await getObjectByIdHash(idHash, obj.$type$, index)) as VersionedObjectResult<
OneVersionedObjectInterfaces[T['$type$']]
>;
}