@snapdm/model
Version:
An opinionated snapshot oriented modeling system for Cloud Firestore
400 lines (388 loc) • 16 kB
TypeScript
import { DeepPartial } from 'ts-essentials';
interface FieldValue {
isEqual(other: FieldValue): boolean;
}
interface FieldValueFactory {
/**
* Returns a sentinel used with `set()` or `update()` to include a
* server-generated timestamp in the written data.
*/
serverTimestamp(): FieldValue;
/**
* Returns a sentinel for use with `update()` to mark a field for deletion.
*/
delete(): FieldValue;
/**
* Returns a special value that can be used with `set()` or `update()` that tells
* the server to union the given elements with any array value that already
* exists on the server. Each specified element that doesn't already exist in
* the array will be added to the end. If the field being modified is not
* already an array it will be overwritten with an array containing exactly
* the specified elements.
*
* @param elements The elements to union into the array.
* @return The FieldValue sentinel for use in a call to `set()` or `update()`.
*/
arrayUnion(...elements: any[]): FieldValue;
/**
* Returns a special value that can be used with `set()` or `update()` that tells
* the server to remove the given elements from any array value that already
* exists on the server. All instances of each element specified will be
* removed from the array. If the field being modified is not already an
* array it will be overwritten with an empty array.
*
* @param elements The elements to remove from the array.
* @return The FieldValue sentinel for use in a call to `set()` or `update()`.
*/
arrayRemove(...elements: any[]): FieldValue;
/**
* Returns a special value that can be used with `set()` or `update()` that tells
* the server to increment the field's current value by the given value.
*
* If either the operand or the current field value uses floating point precision,
* all arithmetic follows IEEE 754 semantics. If both values are integers,
* values outside of JavaScript's safe number range (`Number.MIN_SAFE_INTEGER` to
* `Number.MAX_SAFE_INTEGER`) are also subject to precision loss. Furthermore,
* once processed by the Firestore backend, all integer operations are capped
* between -2^63 and 2^63-1.
*
* If the current field value is not of type `number`, or if the field does not
* yet exist, the transformation sets the field to the given value.
*
* @param n The value to increment by.
* @return The FieldValue sentinel for use in a call to `set()` or `update()`.
*/
increment(n: number): FieldValue;
}
declare type IdFactory = () => string;
declare type DocumentData = Readonly<{
[field: string]: unknown;
}>;
interface DocumentReference<T = DocumentData> {
/** The identifier of the document within its collection. */
readonly id: string;
/**
* A reference to the Collection to which this DocumentReference belongs.
*/
readonly parent: CollectionReference<T>;
/**
* A string representing the path of the referenced document (relative
* to the root of the database).
*/
readonly path: string;
}
/**
* A `CollectionReference` object can be used for adding documents, getting
* document references, and querying for documents (using {@link query}).
*/
interface CollectionReference<T = DocumentData> {
/** The identifier of the collection. */
readonly id: string;
/**
* A reference to the containing Document if this is a subcollection, else
* null.
*/
readonly parent: DocumentReference<T> | null;
/**
* A string representing the path of the referenced collection (relative
* to the root of the database).
*/
readonly path: string;
}
declare type ReferenceFactory = (collection: string, id: string, parent?: DocumentReference) => DocumentReference;
interface Timestamp {
/**
* The number of seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z.
*/
readonly seconds: number;
/**
* The non-negative fractions of a second at nanosecond resolution.
*/
readonly nanoseconds: number;
toDate(): Date;
toMillis(): number;
isEqual(other: Timestamp): boolean;
}
interface TimestampFactory {
/**
* Creates a new timestamp with the current date, with millisecond precision.
*
* @return A new `Timestamp` representing the current date.
*/
now(): Timestamp;
/**
* Creates a new timestamp from the given date.
*
* @param date The date to initialize the `Timestamp` from.
* @return A new `Timestamp` representing the same point in time as the
* given date.
*/
fromDate(date: Date): Timestamp;
/**
* Creates a new timestamp from the given number of milliseconds.
*
* @param milliseconds Number of milliseconds since Unix epoch
* 1970-01-01T00:00:00Z.
* @return A new `Timestamp` representing the same point in time as the
* given number of milliseconds.
*/
fromMillis(milliseconds: number): Timestamp;
}
declare type Adapter = Readonly<{
/**
* The IdGenerator to use when
*/
ids: IdFactory;
fieldValues: FieldValueFactory;
timestamps: TimestampFactory;
references: ReferenceFactory;
}>;
declare function adapter(): Adapter;
interface SnapdmOptions {
readonly adapter: Adapter;
}
declare function initialize(options: SnapdmOptions): void;
declare type SnapshotData = object;
declare type SnapshotAttributes<T extends SnapshotData = SnapshotData> = Readonly<{
type: string;
id: string;
ref: DocumentReference<Snapshot<T>>;
createdAt: Timestamp;
updatedAt: Timestamp;
}>;
/**
* A document snapshot model.
*/
declare type Snapshot<T extends SnapshotData = SnapshotData> = SnapshotAttributes<T> & T;
declare type SnapshotUpdates<Data = SnapshotData> = {
readonly updatedAt: Timestamp;
} & DeepPartial<Data>;
/**
* An object containing error keys and reasons.
*/
declare type ValidationErrors = Record<string, any>;
/**
* A validator is a function that takes a value and return null if that value
* is valid, or an object containing error keys and reasons.
*/
declare type Validator<T> = (value: T) => ValidationErrors | null;
declare type Type<T> = new (...args: any[]) => T;
declare type AbstractType<T> = abstract new (...args: any[]) => T;
declare type AnyType<T> = Type<T> | AbstractType<T>;
declare type ModelIdentifiers = 'type' | 'id' | 'ref';
declare type ModelImmutableAttributes = ModelIdentifiers | 'createdAt' | 'updatedAt';
declare type ModelAttributes<Data extends SnapshotData> = Omit<Data, ModelImmutableAttributes>;
declare type ModelClassAttributes = Readonly<{
type?: string;
collection: string;
}>;
declare type ModelParent<Data extends SnapshotData> = Readonly<{
model: Type<AnyModel> & ModelClassAttributes & Readonly<{
parent?: any;
}>;
attribute: keyof ModelAttributes<Data>;
}>;
declare type InitializeFunction<Data extends SnapshotData, Initializer> = (init: Initializer) => ModelInit<Data>;
declare type InitializeFunctionWithBase<Base extends RootModel & AnyModel, Data extends ModelData<Base>, Initializer> = (init: Initializer) => ModelInit<Data>;
declare type ModelOptions<Data extends SnapshotData, Initializer> = ModelClassAttributes & Readonly<{
/**
* Metadata about this model's parent.
*/
parent?: ModelParent<Data>;
/**
* An initializing function that converts a model's initializer into
* its internal data. This method is where data defaults should be
* set.
*/
initialize?: InitializeFunction<Data, Initializer>;
/**
* An optional list of validators to apply to the model's snapshot
* before writing it to the database.
*/
validators?: Validator<Data>[];
}>;
declare type ModelData<T extends RootModel & AnyModel> = Omit<T['snapshot'], ModelImmutableAttributes>;
declare type ModelWithBaseOptions<Base extends RootModel & AnyModel, Data extends ModelData<Base>, Initializer> = Readonly<{
type?: string;
/**
* An initializing function that converts a model's initializer into
* its internal data. This method is where data defaults should be
* set.
*/
initialize?: InitializeFunctionWithBase<Base, Data, Initializer>;
/**
* An optional list of validators to apply to the model's snapshot
* before writing it to the database.
*/
validators?: Validator<Data>[];
}>;
declare type ExtendedModelOptions<Base extends RootModel & AnyModel, Data extends ModelData<Base>, Initializer> = Readonly<{
extends: AnyType<Base>;
}> & ModelWithBaseOptions<Base, Data, Initializer>;
/**
* A type that accurately represents the interface of typeof AnyModel.
*/
declare type ModelClass<T extends AnyModel> = Type<T> & Exclude<ModelOptions<T['snapshot'], any>, 'validators'> & Readonly<{
validator: Validator<T['snapshot']>;
}>;
declare type AnyModelClass<T extends AnyModel> = AnyType<T> & Exclude<ModelOptions<T['snapshot'], any>, 'validators'> & Readonly<{
validator: Validator<T['snapshot']>;
}>;
declare type ModelInit<Data extends SnapshotData> = Omit<Data, ModelImmutableAttributes> & {
type?: string;
id?: string;
};
/**
* A reference to another model.
*/
declare type ModelRef<T extends AnyModel> = Readonly<{
type: string;
id: string;
ref: DocumentReference<T['snapshot']>;
}>;
interface AnyModel<Data extends SnapshotData = object> {
readonly type: string;
readonly id: string;
readonly ref: DocumentReference<Snapshot<Data>>;
readonly createdAt: Timestamp;
readonly updatedAt: Timestamp;
readonly snapshot: Snapshot<Data>;
readonly updates?: SnapshotUpdates<Data>;
readonly isNew: boolean;
toRef<Keys extends keyof Data>(...includeAttributes: Keys[]): ModelRef<AnyModel<Data>> & Pick<Data, Keys>;
}
declare type ModelCtrOptions<Data extends SnapshotData> = Readonly<{
updates?: SnapshotUpdates<Data>;
isNew?: boolean;
}>;
interface RootModel extends Snapshot {
}
declare abstract class RootModel {
constructor(initializer: any);
constructor(snapshot: any, options?: any);
}
/**
* Mixin function for creating a new model.
* @param options The options for configuring this model
* @returns A model class with the provided options mixed int the class.
*/
declare function Model<Data extends ModelData<Base>, Initializer, Base extends RootModel & AnyModel = any>(options: ModelOptions<Data, Initializer> | ExtendedModelOptions<Base, Data, Initializer>): {
new (init: Initializer): {
/**
* Get the current snapshot of the underlying JSON document.
*/
readonly snapshot: Snapshot<Data>;
/**
* A flag indicating if this model is new i.e. it was created from
* and initializer and not a snapshot.
*/
readonly isNew: boolean;
/**
* An object containing the differences between this object and
* the object it was copied from. An entity should only be saved if it
* isNew or its updates are defined. This object can also be used
* perform a partial update of the underlying document.
*/
readonly updates?: SnapshotUpdates<Data>;
/**
* Meta method for getting the constructor of `this` object to access static
* methods defined on subclasses, or construct instances of subclasses in
* the base class.
*/
readonly model: ModelClass<any>;
/**
* Convert this model into a reference object.
* @param includeAttributes Optional fields in this model to include in the reference.
* @returns A ref to this model.
*/
toRef<Keys extends "type" | "id" | "ref" | "createdAt" | "updatedAt" | keyof Data>(...includeAttributes: Keys[]): Readonly<{
type: string;
id: string;
ref: DocumentReference<Snapshot<Data>>;
}> & Pick<Snapshot<Data>, Keys>;
/**
* Creates a new instance of this Model containing the provided
* updates to the internal snapshot. This method is not intended to be
* consumed externally to the class and exists primarily to be used as
* an implementation detail of more domain oriented transformations.
* @param updates A patch to apply to the current snapshot in creating
* the new one.
* @returns A new model with the given patch applied.
*/
clone(updates?: DeepPartial<Data>): this;
readonly type: string;
readonly id: string;
readonly ref: DocumentReference<Snapshot<object>>;
readonly createdAt: Timestamp;
readonly updatedAt: Timestamp;
};
new (snapshot: Snapshot<Data>, options?: ModelCtrOptions<Data>): {
/**
* Get the current snapshot of the underlying JSON document.
*/
readonly snapshot: Snapshot<Data>;
/**
* A flag indicating if this model is new i.e. it was created from
* and initializer and not a snapshot.
*/
readonly isNew: boolean;
/**
* An object containing the differences between this object and
* the object it was copied from. An entity should only be saved if it
* isNew or its updates are defined. This object can also be used
* perform a partial update of the underlying document.
*/
readonly updates?: SnapshotUpdates<Data>;
/**
* Meta method for getting the constructor of `this` object to access static
* methods defined on subclasses, or construct instances of subclasses in
* the base class.
*/
readonly model: ModelClass<any>;
/**
* Convert this model into a reference object.
* @param includeAttributes Optional fields in this model to include in the reference.
* @returns A ref to this model.
*/
toRef<Keys extends "type" | "id" | "ref" | "createdAt" | "updatedAt" | keyof Data>(...includeAttributes: Keys[]): Readonly<{
type: string;
id: string;
ref: DocumentReference<Snapshot<Data>>;
}> & Pick<Snapshot<Data>, Keys>;
/**
* Creates a new instance of this Model containing the provided
* updates to the internal snapshot. This method is not intended to be
* consumed externally to the class and exists primarily to be used as
* an implementation detail of more domain oriented transformations.
* @param updates A patch to apply to the current snapshot in creating
* the new one.
* @returns A new model with the given patch applied.
*/
clone(updates?: DeepPartial<Data>): this;
readonly type: string;
readonly id: string;
readonly ref: DocumentReference<Snapshot<object>>;
readonly createdAt: Timestamp;
readonly updatedAt: Timestamp;
};
readonly type: string;
readonly collection: string;
readonly parent: Readonly<{
model: Type<AnyModel<object>> & Readonly<{
type?: string;
collection: string;
}> & Readonly<{
parent?: any;
}>;
attribute: Exclude<keyof Data, ModelImmutableAttributes>;
}>;
readonly initialize: InitializeFunction<Data, Initializer> | typeof identity;
readonly validator: Validator<Data>;
};
declare function identity<T>(e: T): T;
declare type ModelFactory<T extends AnyModel> = ModelClass<T> | Readonly<{
type: AnyModelClass<T>;
factory: (data: DocumentData) => T;
}>;
declare function buildModel<T extends AnyModel>(factory: ModelFactory<T>, data: DocumentData): T;
export { Adapter, AnyModel, InitializeFunction, Model, ModelClass, ModelFactory, ModelOptions, ModelRef, SnapdmOptions, Timestamp, ValidationErrors, Validator, adapter, buildModel, initialize };