diff --git a/types/redux-orm/Model.d.ts b/types/redux-orm/Model.d.ts new file mode 100644 index 0000000000..6ae3d609a2 --- /dev/null +++ b/types/redux-orm/Model.d.ts @@ -0,0 +1,545 @@ +import { ModelTableOpts, TableOpts } from './db'; +import { IdAttribute } from './db/Table'; +import { AttributeWithDefault, FieldSpecMap, ForeignKey, ManyToMany, OneToOne } from './fields'; +import { Optional, OptionalKeys, Overwrite, PickByValue } from './helpers'; +import { IdOrModelLike, ModelField } from './index'; +import QuerySet, { LookupSpec, MutableQuerySet, SortIteratee, SortOrder } from './QuerySet'; +import { OrmSession } from './Session'; + +/** + * A primitive value + */ +export type Primitive = number | string | boolean; + +/** + * Serializable value: a primitive, undefined, a serializable object or an array of those + */ +export type Serializable = + | Primitive + | Primitive[] + | undefined + | { + [K: string]: Serializable | Serializable[]; + }; + +/** + * Object restricted to serializable properties only + */ +export interface SerializableMap { + [K: string]: Serializable | Serializable[]; +} + +/** + * A union of supported model field types + * + * Specify foreign key and one-to-one association properties as Model typed properties. + * + * Specify many-to-many and reverse-fk associations as related Model's specification of: + * - {@link MutableQuerySet} - for many-to-many relations + * - {@link QuerySet} - for reverse side of foreign keys + */ +export type ModelField = MutableQuerySet | QuerySet | SessionBoundModel | Serializable; + +/** + * Map of fields restriction to supported field types. + */ +export interface ModelFieldMap { + [K: string]: ModelField; +} + +/** + * A Model-derived mapped type for supplying relations and alike. + * + * Either a primitive type matching Model's identifier type or a map containing an {IdAttribute: IdType} pair, + * where IdAttribute and IdType match respective Model property key and type + */ +export type IdOrModelLike = IdType | IdEntry; + +/** + * The heart of an ORM, the data model. + * + * The fields you specify to the Model will be used to generate + * a schema to the database, related property accessors, and + * possibly through models. + * + * In each {@link Session} you instantiate from an {@link ORM} instance, + * you will receive a session-specific subclass of this Model. The methods + * you define here will be available to you in sessions. + * + * An instance of {@link Model} represents a record in the database, though + * it is possible to generate multiple instances from the same record in the database. + * + * To create data models in your schema, subclass {@link Model}. To define + * information about the data model, override static class methods. Define instance + * logic by defining prototype methods (without `static` keyword). + * @borrows {@link QuerySet.filter} as Model#filter + */ +export default class Model { + /** + * A string constant identifying specific Model, necessary to retain the shape of state and relations through transpilation steps + */ + static modelName: string; + + /** + * Model field descriptors. + * @see {@link Attribute} + * @see {@link OneToOne} + * @see {@link ForeignKey} + * @see {@link ManyToMany} + */ + static fields: FieldSpecMap; + + /** + * Returns the options object passed to the database for the table that represents + * this Model class. + * + * Returns an empty object by default, which means the database + * will use default options. You can either override this function to return the options + * you want to use, or assign the options object as a static property of the same name to the + * Model class. + * + * @return the options object passed to the database for the table + * representing this Model class. + */ + static options: { (): TableOpts } | TableOpts; + + /** + * The key of Model's identifier property + */ + static readonly idAttribute: string; + /** + * {@link QuerySet} class associated with this Model class. + * + * Defaults to base {@link QuerySet} + */ + static querySetClass: typeof QuerySet; + + /** + * @see {@link Model.getQuerySet} + */ + static readonly query: QuerySet; + /** + * Returns a reference to the plain JS object in the store. + * Make sure to not mutate this. + * + * @return a reference to the plain JS object in the store + */ + readonly ref: Ref>; + + /** + * Creates a Model instance from it's properties. + * Don't use this to create a new record; Use the static method {@link Model#create}. + * @param props - the properties to instantiate with + */ + constructor(props: Fields); + + /** + * Model specific reducer function. + * + * An alternative to standalone reducer function. + * + * @see {@link createReducer} + * + * @param action - store-dispatched action instance + * @param modelType - a {@link ModelType} parametrized with a + * {@link Model} type that the reducer is being attached to. + * @param session - an optional parameter, can be used for querying other Models (mutations are not supported) + */ + static reducer(action: any, modelType: ModelType, session: OrmSession): void; + + /** + * Creates a new record in the database, instantiates a {@link Model} and returns it. + * + * If you pass values for many-to-many fields, instances are created on the through + * model as well. + * + * @param userProps - the new {@link Model}'s properties. + * @return a new {@link SessionBoundModel} instance. + */ + static create>(userProps: TProps): SessionBoundModel; + + /** + * Creates a new or update existing record in the database, instantiates a {@link Model} and returns it. + * + * If you pass values for many-to-many fields, instances are created on the through + * model as well. + * + * @param userProps - the upserted {@link Model}'s properties. + * @return a {@link SessionBoundModel} instance. + */ + static upsert>(userProps: TProps): SessionBoundModel; + + /** + * Gets the {@link Model} instance that matches properties in `lookupObj`. + * Throws an error if {@link Model} if multiple records match + * the properties. + * + * @param lookupObj - the properties used to match a single entity. + * @throws {Error} If more than one entity matches the properties in `lookupObj`. + * @return a {@link SessionBoundModel} instance that matches the properties in `lookupObj`. + */ + static get>( + lookupObj: TProps + ): SessionBoundModel | null; + + /** + * Returns a {@link Model} instance for the object with id `id`. + * Returns `null` if the model has no instance with id `id`. + * + * You can use {@link Model#idExists} to check for existence instead. + * + * @param id - the `id` of the object to get + * @return a {@link SessionBoundModel} instance with id `id` + */ + static withId(id: IdType): SessionBoundModel | null; + + /** + * Returns a boolean indicating if an entity + * with the id `id` exists in the state. + * + * @param id - a value corresponding to the id attribute of the {@link Model} class. + * @return a boolean indicating if entity with `id` exists in the state + * + * @since 0.11.0 + */ + static idExists(id: string | number): boolean; + + /** + * @return A string representation of this {@link Model} class. + */ + static toString(): string; + + /** + * Manually mark individual instances as accessed. + * This allows invalidating selector memoization within mutable sessions. + * + * @param ids - Array of primary key values + */ + static markAccessed(ids: Array): void; + + /** + * Manually mark this model's table as scanned. + * This allows invalidating selector memoization within mutable sessions. + * + */ + static markFullTableScanned(): void; + + /** + * Returns an instance of the model's `querySetClass` field. + * By default, this will be an empty {@link QuerySet}. + * + * @return An instance of the model's `querySetClass`. + */ + static getQuerySet(): QuerySet; + + /** + * @see {@link QuerySet.all} + */ + static all(this: ModelType): QuerySet; + + /** + * @see {@link QuerySet.at} + */ + static at(index: number): SessionBoundModel | undefined; + + /** + * @see {@link QuerySet.first} + */ + static first(): SessionBoundModel | undefined; + + /** + * @see {@link QuerySet.last} + */ + static last(): SessionBoundModel | undefined; + + /** + * @see {@link QuerySet.update} + */ + static update(props: UpdateProps): void; + + /** + * @see {@link QuerySet.filter} + */ + static filter(props: LookupSpec): QuerySet; + + /** + * @see {@link QuerySet.exclude} + */ + static exclude(props: LookupSpec): QuerySet; + + /** + * @see {@link QuerySet.orderBy} + */ + static orderBy(iteratees: ReadonlyArray>, orders?: ReadonlyArray): QuerySet; + + /** + * @see {@link QuerySet.count} + */ + static count(): number; + + /** + * @see {@link QuerySet.exists} + */ + static exists(): boolean; + + /** + * @see {@link QuerySet.delete} + */ + static delete(): void; + + /** + * Gets the {@link Model} class or subclass constructor (the class that + * instantiated this instance). + * + * @return The {@link Model} class or subclass constructor used to instantiate + * this instance. + */ + getClass(): MClass; + + /** + * Gets the id value of the current instance by looking up the id attribute. + * @return The id value of the current instance. + */ + getId(): string | number; + + /** + * @return A string representation of this {@link Model} instance. + */ + toString(): string; + + /** + * Returns a boolean indicating if `otherModel` equals this {@link Model} instance. + * Equality is determined by shallow comparing their attributes. + * + * This equality is used when you call {@link Model#update}. + * You can prevent model updates by returning `true` here. + * However, a model will always be updated if its relationships are changed. + * + * @param otherModel - a {@link Model} instance to compare + * @return a boolean indicating if the {@link Model} instance's are equal. + */ + equals(otherModel: Model | SessionBoundModel): boolean; + + /** + * Updates a property name to given value for this {@link Model} instance. + * The values are immediately committed to the database. + * + * @param propertyName - name of the property to set + * @param value - value assigned to the property + */ + set(propertyName: K, value: RefPropOrSimple, K>): void; + + /** + * Assigns multiple fields and corresponding values to this {@link Model} instance. + * The updates are immediately committed to the database. + * + * @param userMergeObj - an object that will be merged with this instance. + */ + update(userMergeObj: UpdateProps>): void; + + /** + * Updates {@link Model} instance attributes to reflect the + * database state in the current session. + */ + refreshFromState(): void; + + /** + * Deletes the record for this {@link Model} instance. + * Fields and values on the instance are still accessible after the call. + */ + delete(): void; +} + +/** + * Model wildcard type. + */ +export class AnyModel extends Model {} + +/** + * {@link Model#upsert} argument type + * + * Relations can be provided in a flexible manner for both many-to-many and foreign key associations + * @see {@link IdOrModelLike} + */ +export type UpsertProps = Overwrite>, Required>>; + +/** + * {@link Model#update} argument type + * + * All properties are optional. + * Supplied properties are type-checked against the type of related Model's fields. + * Relations can be provided in a flexible manner for both many-to-many and foreign key associations + * @see {@link IdOrModelLike} + */ +export type UpdateProps = Omit, IdKey>; + +/** + * @internal + */ +export type CustomInstanceProps = PickByValue< + Omit>>, + Serializable +>; + +/** + * Model id property key extraction helper. + * + * Falls back to `'id'` if not specified explicitly via {@link Model.options}. + */ +export type IdKey = IdAttribute>; + +/** + * Model id property type extraction helper. + * + * Falls back to `number` if not specified explicitly via {@link Model.options}. + */ +export type IdType = IdKey extends infer U + ? U extends keyof ModelFields + ? ModelFields[U] extends string | number + ? ModelFields[U] + : never + : number + : number; + +/** + * A single entry map representing IdKey: IdType property of supplied {@link Model}. + */ +export type IdEntry = { [K in IdKey]: IdType }; + +/** + * Type of {@link Model.ref} / database entry for a particular Model type + */ +export type Ref = { + [K in keyof RefFields]: ModelFields[K] extends AnyModel ? IdType[K]> : RefFields[K] +}; + +/** + * A mapped type restricting allowed types of second {@link Model.set} argument. + * Depending on the first argument `propertyName` argument, value type can be restricted to: + * - declared Model field type - if propertyName belongs to declared Model fields + * - any serializable value - if propertyName is not among declared Model fields + */ +export type RefPropOrSimple = K extends keyof RefFields + ? Ref[K] + : Serializable; + +/** + * A Model-derived mapped type, representing model instance bound to a session. + * + * SessionBoundModels relation properties for convenient association traversal. + * Custom type-checked properties are available on `SessionBoundModel` instances created using + * @link Model#create} or {@link Model#upsert} calls. + */ +export type SessionBoundModel = M & + { [K in keyof ModelFields]: SessionBoundModelField } & + CustomInstanceProps; + +/** + * Static side of a particular {@link Model} with member signatures narrowed to provided {@link Model} type + * + * @template M a model type narrowing static {@link Model} member signatures. + * + * @inheritDoc + */ +export interface ModelType extends QuerySet { + new (props: ModelFields): SessionBoundModel; + + options: ModelTableOpts>; + + modelName: ModelClass['modelName']; + + fields: ModelClass['fields']; + + /** + * @see {@link Model#idExists} + */ + idExists(id: IdType): boolean; + + /** + * @see {@link Model#withId} + */ + withId(id: IdType): SessionBoundModel | null; + + /** + * @see {@link Model#get} + */ + get>(lookupSpec: TLookup): SessionBoundModel | null; + + /** + * @see {@link Model#create} + */ + create>(props: TProps): SessionBoundModel; + + /** + * @see {@link Model#upsert} + */ + upsert>(props: TProps): SessionBoundModel; +} + +/** + * @internal + */ +export type ModelClass = ReturnType; + +/** + * @internal + */ +export type ModelFields = [ConstructorParameters>] extends [[infer U]] + ? U extends ModelFieldMap + ? U + : never + : never; + +/** + * @internal + */ +export type FieldSpecKeys = keyof PickByValue['fields'], TField>; + +/** + * @internal + */ +export type RefFields = keyof ModelFields> = Omit< + ModelFields, + Extract> +>; + +/** + * @internal + */ +export type SessionBoundModelField> = ModelFields< + M +>[K] extends AnyModel + ? SessionBoundModel[K]> + : ModelFields[K]; + +/** + * {@link Model#create} argument type + * + * Relations can be provided in a flexible manner for both many-to-many and foreign key associations + * @see {@link IdOrModelLike} + */ + +export type CreateProps< + M extends AnyModel, + RFields extends Required> = Required> +> = Optional< + { + [K in keyof ModelFields]: { + [P in K]: RFields[P] extends MutableQuerySet + ? ReadonlyArray> + : (RFields[P] extends QuerySet + ? never + : RFields[P] extends AnyModel + ? (P extends FieldSpecKeys ? IdOrModelLike : never) + : RFields[P]) + }[K] + }, + OptionalCreatePropsKeys +>; + +/** + * @internal + */ +export type OptionalCreatePropsKeys = IdType extends number + ? (IdKey | OptionalKeys> | FieldSpecKeys) + : (OptionalKeys> | FieldSpecKeys); diff --git a/types/redux-orm/ORM.d.ts b/types/redux-orm/ORM.d.ts new file mode 100644 index 0000000000..db60cd0681 --- /dev/null +++ b/types/redux-orm/ORM.d.ts @@ -0,0 +1,115 @@ +import { Database, DatabaseCreator, TableState } from './db'; +import { AnyModel } from './Model'; +import Session, { OrmSession } from './Session'; + +/** + * A `{typeof Model[modelName]: typeof Model}` map defining: + * + * - database schema + * - {@link Session} bound Model classes + * - ORM branch state type + */ +export type IndexedModelClasses< + T extends { [k in keyof T]: typeof AnyModel }, + K extends keyof T = Extract +> = { [k in K]: T[K] }; + +/** + * A mapped type capable of inferring ORM branch state type based on schema {@link Model}s. + */ +export type OrmState> = { [K in keyof MClassMap]: TableState }; + +/** + * ORM instantiation opts. + * + * Enables customization of database creation. + */ +export interface ORMOpts { + createDatabase: DatabaseCreator; +} + +/** + * ORM - the Object Relational Mapper. + * + * Use instances of this class to: + * + * - Register your {@link Model} classes using {@link ORM#register} + * - Get the empty state for the underlying database with {@link ORM#getEmptyState} + * - Start an immutable database session with {@link ORM#session} + * - Start a mutating database session with {@link ORM#mutableSession} + * + * Internally, this class handles generating a schema specification from models + * to the database. + */ +export class ORM, ModelNames extends keyof I = keyof I> { + /** + * Creates a new ORM instance. + */ + constructor(opts?: ORMOpts); + + /** + * Registers a {@link Model} class to the ORM. + * + * If the model has declared any ManyToMany fields, their + * through models will be generated and registered with + * this call, unless a custom through model has been specified. + * + * @param model - a {@link Model} class to register + */ + register(...model: ReadonlyArray): void; + + /** + * Gets a {@link Model} class by its name from the registry. + * + * @param modelName - the name of the {@link Model} class to get + * + * @throws If {@link Model} class is not found. + * + * @return the {@link Model} class, if found + */ + get(modelName: K): I[K]; + + /** + * Returns the empty database state. + * + * @see {@link OrmState} + * + * @return empty state + */ + getEmptyState(): OrmState; + + /** + * Begins an immutable database session. + * + * @see {@link OrmState} + * @see {@link SessionType} + * + * @param state - the state the database manages + * + * @return a new {@link Session} instance + */ + session(state: OrmState): OrmSession; + + /** + * Begins an mutable database session. + * + * @see {@link OrmState} + * @see {@link SessionType} + * + * @param state - the state the database manages + * + * @return a new {@link Session} instance + */ + mutableSession(state: OrmState): OrmSession; + + /** + * Acquire database reference. + * + * If no database exists, an instance is created using either default or supplied implementation of {@link DatabaseCreator}. + * + * @return A {@link Database} instance structured according to registered schema. + */ + getDatabase(): Database; +} + +export default ORM; diff --git a/types/redux-orm/QuerySet.d.ts b/types/redux-orm/QuerySet.d.ts new file mode 100644 index 0000000000..7cf9a77f1a --- /dev/null +++ b/types/redux-orm/QuerySet.d.ts @@ -0,0 +1,264 @@ +import { QueryClause } from './db'; +import Model, { + AnyModel, + CustomInstanceProps, + IdOrModelLike, + ModelClass, + Ref, + SessionBoundModel, + UpdateProps +} from './Model'; + +/** + * Optional ordering direction. + * + * {@see QuerySet.orderBy} + */ +export type SortOrder = 'asc' | 'desc' | true | false; + +/** + * Ordering clause. + * + * Either a key of SessionBoundModel or a evaluator function accepting plain object Model representation stored in the database. + * + * {@see QuerySet.orderBy} + */ +export type SortIteratee = keyof Ref | { (row: Ref): any }; + +/** + * Lookup clause as an object specifying props to match with plain object Model representation stored in the database. + * {@see QuerySet.exclude} + * {@see QuerySet.filter} + */ +export type LookupProps = Partial>; + +/** + * Lookup clause as predicate accepting plain object Model representation stored in the database. + * {@see QuerySet.exclude} + * {@see QuerySet.filter} + */ +export type LookupPredicate = (row: Ref) => boolean; + +/** + * A union of lookup clauses. + * {@see QuerySet.exclude} + * {@see QuerySet.filter} + */ +export type LookupSpec = LookupProps | LookupPredicate; + +/** + * A lookup query result. + * + * May contain additional properties in case {@link LookupProps} clause had been supplied. + * {@see QuerySet.exclude} + * {@see QuerySet.filter} + */ +export type LookupResult> = TLookup extends LookupPredicate + ? QuerySet + : QuerySet>>; + +/** + *

+ * `QuerySet` class is used to build and make queries to the database + * and operating the resulting set (such as updating attributes + * or deleting the records). + *

+ * + * @example queries are built lazily + * const qs = Book.all() + * .filter(book => book.releaseYear > 1999) + * .orderBy('name') + * + * @description The query is executed only when terminating operations are invoked, such as: + * + * - {@link QuerySet#count}, + * - {@link QuerySet#toRefArray} + * - {@link QuerySet#at} and other indexed reads + * + * After the query is executed, the resulting set is cached in the QuerySet instance. + * + * QuerySet instances return copies, so chaining filters doesn't + * mutate the previous instances. + * + * @template M type of {@link Model} instances returned by QuerySet's methods. + * @template InstanceProps additional properties available on QuerySet's elements. + */ +export default class QuerySet { + /** + * Creates a `QuerySet`. The constructor is mainly for internal use; + * Access QuerySet instances from {@link Model}. + * + * @param modelClass - the {@link Model} class of objects in this QuerySet. + * @param clauses - query clauses needed to evaluate the set. + * @param [opts] - additional options + */ + constructor(modelClass: ModelClass, clauses: QueryClause[], opts?: object); + + /** + * Register custom method on a `QuerySet` class specification. + * QuerySet class may be attached to a {@link Model} class via {@link Model#querySetClass} + * + * @param methodName - name of a method to be available on specific QuerySet class instances + * + * @example: + * class CustomQuerySet extends QuerySet { + * static currentYear = 2019 + * unreleased(): QuerySet { + * return this.filter(book => book.releaseYear > CustomQuerySet.currentYear); + * } + * } + * CustomQuerySet.addSharedMethod('unreleased'); + * // assign specific QuerySet to a Model class + * Book.querySetClass = typeof CustomQuerySet; + * // register models + * const schema = {Book }; + * const orm = new ORM(); + * orm.register(Book); + * const session = orm.session(orm.getEmptyState()); + * // use shared method + * const unreleased = customQs.unreleased(); + */ + static addSharedMethod(methodName: string): void; + + /** + * Returns a new {@link QuerySet} instance with the same entities. + * @return a new QuerySet with the same entities. + */ + all(): QuerySet; + + /** + * Returns the {@link SessionBoundModel} instance at index `index` in the {@link QuerySet} instance if + * `withRefs` flag is set to `false`, or a reference to the plain JavaScript + * object in the model state if `true`. + * + * @param index - index of the model instance to get + * @return a {@link Model} derived {@link SessionBoundModel} instance at index + * `index` in the {@link QuerySet} instance, + * or undefined if the index is out of bounds. + */ + at(index: number): SessionBoundModel | undefined; + + /** + * Returns the session bound {@link Model} instance at index 0 + * in the {@link QuerySet} instance or undefined if the instance is empty. + * + * @return a {@link Model} derived {@link SessionBoundModel} instance or undefined. + */ + first(): SessionBoundModel | undefined; + + /** + * Returns the session bound {@link Model} instance at index `QuerySet.count() - 1` + * in the {@link QuerySet} instance or undefined if the instance is empty. + * + * @return a {@link Model} derived {@link SessionBoundModel} instance or undefined. + */ + last(): SessionBoundModel | undefined; + + /** + * Returns a new {@link QuerySet} instance with entities that match properties in `lookupObj`. + * + * @param lookupObj - the properties to match objects with ({@link LookupProps}). + * Can also be a function ({@link LookupPredicate}). + * + * @return a new {@link QuerySet} instance with objects that passed the filter. + */ + filter>(lookupObj: TLookup): LookupResult; + + /** + * Returns a new {@link QuerySet} instance with entities that do not match properties in `lookupObj`. + * + * @param lookupObj - the properties to match objects with ({@link LookupProps}). + * Can also be a function ({@link LookupPredicate}). + * + * @return a new {@link QuerySet} instance with objects that passed the filter. + */ + exclude>(lookupObj: TLookup): LookupResult; + + /** + * Returns a new {@link QuerySet} instance with entities ordered by `iteratees` in ascending + * order, unless otherwise specified. Delegates to `lodash.orderBy`. + * + * @param iteratees - an array or a single {@link SortIteratee} where each item can be a string or a + * function. If a string is supplied, it should + * correspond to property on the entity that will + * determine the order. If a function is supplied, + * it should return the value to order by. + * + * @param [orders] - the sort orders of `iteratees`. If unspecified, all iteratees + * will be sorted in ascending order. `true` and `'asc'` + * correspond to ascending order, and `false` and `'desc` + * to descending order. Accepts an array or a single {@link SortOrder}. + * + * @return a new {@link QuerySet} with objects ordered by `iteratees`. + */ + orderBy( + iteratees: SortIteratee | ReadonlyArray>, + orders?: SortOrder | ReadonlyArray + ): QuerySet; + + /** + * Returns the number of {@link Model} instances represented by the QuerySet. + * + * @return length of the QuerySet + */ + count(): number; + + /** + * Checks if the {@link QuerySet} instance has any records matching the query + * in the database. + * + * @return `true` if the {@link QuerySet} instance contains entities, else `false`. + */ + exists(): boolean; + + /** + * Returns an array of the plain objects represented by the QuerySet. + * The plain objects are direct references to the store. + * + * @return references to the plain JS objects represented by the QuerySet + */ + toRefArray(): ReadonlyArray>; + + /** + * Returns an array of {@link SessionBoundModel} instances represented by the QuerySet. + * + * @return session bound model instances represented by the QuerySet + */ + toModelArray(): ReadonlyArray>; + + /** + * Records an update specified with `mergeObj` to all the objects + * in the {@link QuerySet} instance. + * + * @param mergeObj - an object extending {@link UpdateProps}, to be merged with all the objects in this QuerySet. + */ + update(mergeObj: UpdateProps): void; + + /** + * Records a deletion of all the objects in this {@link QuerySet} instance. + */ + delete(): void; + + /** + * Returns a string representation of QuerySet instance contents. + * + * @return string representation of QuerySet. + */ + toString(): string; +} + +/** + * {@link QuerySet} extensions available on {@link ManyToMany} fields of session bound {@link Model} instances. + */ +export interface ManyToManyExtensions { + add: (...entitiesToAdd: ReadonlyArray>) => void; + remove: (...entitiesToRemove: ReadonlyArray>) => void; + clear: () => void; +} + +/** + * A {@link QuerySet} extended with {@link ManyToMany} specific functionality - {@link ManyToManyExtensions}. + */ +export interface MutableQuerySet + extends ManyToManyExtensions, + QuerySet {} diff --git a/types/redux-orm/Session.d.ts b/types/redux-orm/Session.d.ts new file mode 100644 index 0000000000..184db48a62 --- /dev/null +++ b/types/redux-orm/Session.d.ts @@ -0,0 +1,72 @@ +import { IndexedModelClasses, ORM, OrmState } from './ORM'; +import { Database, QueryResult, QuerySpec, UpdateSpec } from './db'; +import { Assign } from './helpers'; +import { ModelType } from './Model'; + +export type BatchToken = any; + +export default class Session> { + /** + * list of bound {@link Model} classes bound to this session, bootstrapped during {@link @ORM.register}. + * + * @see {@link ModelType} + */ + readonly sessionBoundModels: ReadonlyArray>>; + + /** + * Current {@link OrmState}, specific to registered schema + */ + readonly state: OrmState; + + /** + * Creates a new Session. + * + * @param schema - {@Link ORM} instance, with bootstrapped {@link Model} prototypes. + * @param db - a {@link Database} instance + * @param state - the database {@link OrmState} + * @param withMutations? - whether the session should mutate data + * @param batchToken? - a {@link BatchToken} used by the backend to identify objects that can be + * mutated. If none is provided a default of `Symbol('ownerId')` will be created. + * + */ + constructor(schema: ORM, db: Database, state: OrmState, withMutations?: boolean, batchToken?: BatchToken); + + /** + * Executes query against model state. + * + * @param query - the query command object. + * + * @returns query result. + * + * @see {@link QueryType} + * @see {@link QueryClause} + * @see {@link QuerySpec} + * @see {@link QueryResult} + */ + query(query: QuerySpec): QueryResult; + + /** + * Applies update to a model state. + * + * @param update - the update command object. + * + * @returns query result. + * + * @see {@link DbAction} + * @see {@link UpdateSpec} + * @see {@link DbActionResult} + * @see {@link UpdateResult} + */ + applyUpdate

(update: UpdateSpec

): P; +} + +/** + * An {@link ORM}-bound {@link Session} instance, extended with a set of {@link ModelType} properties. + * + * Extension is a map of {@link ModelType} accessible under keys within a set of {@link Model#modelName} values + * for registered {@link Model} classes. + */ +export type OrmSession> = Assign< + Session, + { [K in keyof I]: ModelType> } +>; diff --git a/types/redux-orm/constants.d.ts b/types/redux-orm/constants.d.ts new file mode 100644 index 0000000000..427f3cfee1 --- /dev/null +++ b/types/redux-orm/constants.d.ts @@ -0,0 +1,10 @@ +export const UPDATE = 'REDUX_ORM_UPDATE'; +export const DELETE = 'REDUX_ORM_DELETE'; +export const CREATE = 'REDUX_ORM_CREATE'; + +export const FILTER = 'REDUX_ORM_FILTER'; +export const EXCLUDE = 'REDUX_ORM_EXCLUDE'; +export const ORDER_BY = 'REDUX_ORM_ORDER_BY'; + +export const SUCCESS = 'SUCCESS'; +export const FAILURE = 'FAILURE'; diff --git a/types/redux-orm/db/Database.d.ts b/types/redux-orm/db/Database.d.ts new file mode 100644 index 0000000000..514c5e25cf --- /dev/null +++ b/types/redux-orm/db/Database.d.ts @@ -0,0 +1,160 @@ +import { IndexedModelClasses, OrmState } from '../ORM'; +import { CREATE, DELETE, EXCLUDE, FAILURE, FILTER, ORDER_BY, SUCCESS, UPDATE } from '../constants'; +import { ModelTableOpts, Table } from './Table'; +import { SerializableMap } from '../Model'; +import { BatchToken } from '../Session'; + +/** + * A type of {@link QueryClause}. + */ +export type QueryType = typeof FILTER | typeof EXCLUDE | typeof ORDER_BY; + +/** + * A single `QueryClause`. + * Multiple `QueryClause`s can be combined into a {@link Query}. + */ +export interface QueryClause { + type: QueryType; + payload: Payload; +} + +/** + * Query definition, contains target table and a collection of {@link QueryClause}. + */ +export interface Query { + table: string; + clauses: QueryClause[]; +} + +/** + * Query wrapper definition, wraps {@link Query}. + */ +export interface QuerySpec { + query: Query; +} + +/** + * Query result. + */ +export interface QueryResult { + rows: ReadonlyArray; +} + +/** + * A type of data update to perform. + */ +export type UpdateType = typeof CREATE | typeof UPDATE | typeof DELETE; + +/** + * A status of data update operation. + */ +export type UpdateStatus = typeof SUCCESS | typeof FAILURE; + +/** + * Data update definition + */ +export interface UpdateSpec { + action: UpdateType; + payload?: Payload; + query?: Query; +} + +/** + * Data update result. + */ +export interface UpdateResult, Payload extends object = {}> { + status: UpdateStatus; + state: OrmState; + payload: Payload; +} + +/** + * Transactions aggregate batches of operations. + */ +export interface Transaction { + batchToken: BatchToken; + withMutations: boolean; +} + +/** + * Schema specification, required for default database creator. + * + * @see {@link DatabaseCreator} + * @see {@link ModelTableOpts} + * @see {@link Table} + */ +export interface SchemaSpec> { + tables: { [K in keyof I]: ModelTableOpts }; +} + +/** + * A Database parametrized by schema made of {@link Model} classes. + * + * @see {@link SchemaSpec} + * @see {@link TableSpec} + * @see {@link Table} + */ +export interface Database, Tables = { [K in keyof I]: Table }> { + /** + * Returns the empty database state. + * + * @see {@link OrmState} + * + * @return empty state + */ + getEmptyState(): OrmState; + + /** + * Execute a query against a given state. + * + * @param querySpec - a query definition. + * @param state - the state to query against. + * + * @see {@link QuerySpec} + * @see {@link OrmState} + * @see {@link OrmState} + * + * @return a {@link QueryResult} containing 0 to many {@link QueryResult.rows}. + */ + query(querySpec: QuerySpec, state: OrmState): QueryResult; + + /** + * Apply an update to a given state. + * + * @param updateSpec - a data update definition. + * @param tx - a transaction for batches of operations. + * @param state - the state to apply update to. + * + * @see {@link UpdateSpec} + * @see {@link Transaction} + * @see {@link OrmState} + * + * @return a {@link UpdateResult} containing 0 to many {@link QueryResult.rows}. + */ + + update(updateSpec: UpdateSpec, tx: Transaction, state: OrmState): UpdateResult; + + /** + * Return a {@link Table} structure based on provided table name. + * @param tableName - the name of the {@link Table} to describe + * + * @return a {@link Table} instance matching given `tableName` or `undefined` if no such table exists. + */ + describe(tableName: K): Tables[K]; +} + +/** + * Database creation function type. + */ +export type DatabaseCreator = typeof createDatabase; + +/** + * Default database creation procedure handle. + * + * @param schemaSpec - a {@link SchemaSpec} to built the {@link Database} from. + * + * @return a {@Link Database} instance, ready for query and data update operation. + */ +export function createDatabase>(schemaSpec: SchemaSpec): Database; + +export default createDatabase; diff --git a/types/redux-orm/db/Table.d.ts b/types/redux-orm/db/Table.d.ts new file mode 100644 index 0000000000..80365a0e14 --- /dev/null +++ b/types/redux-orm/db/Table.d.ts @@ -0,0 +1,126 @@ +import Model, { AnyModel, FieldSpecKeys, IdType, Ref } from '../Model'; +import { ForeignKey, OneToOne, TableOpts } from '../index'; +import { Field } from '../fields'; + +/** + * {@link TableOpts} used for {@link Table} customization. + * + * Supplied via {@link Model#options}. + * + * If no customizations were provided, the table uses following default options: + *
+ * ```typescript + * { + * idAttribute: 'id', + * arrName: 'items', + * mapName: 'itemsById' + * } + * ``` + *
+ * @see {@link Model} + * @see {@link Model#options} + * @see {@link OrmState} + */ +export interface TableOpts { + readonly idAttribute?: string; + readonly arrName?: string; + readonly mapName?: string; + readonly fields?: { [K: string]: Field }; +} + +/** + * @internal + */ +export type ExtractModelOption< + MClass extends typeof AnyModel, + K extends keyof TableOpts, + DefaultValue extends string +> = MClass['options'] extends () => { [P in K]: infer R } + ? R extends string + ? R + : DefaultValue + : MClass['options'] extends { [P in K]: infer R } + ? R extends string + ? R + : DefaultValue + : DefaultValue; + +/** + * Model idAttribute option extraction helper. + * + * Falls back to `'id'` if not specified explicitly via {@link Model.options}. + */ +export type IdAttribute = ExtractModelOption; + +/** + * Model arrName option extraction helper. + * + * Falls back to `'items'` if not specified explicitly via {@link Model.options}. + */ +export type ArrName = ExtractModelOption; + +/** + * Model mapName option extraction helper. + * + * Falls back to `'itemsById'` if not specified explicitly via {@link Model.options}. + */ +export type MapName = ExtractModelOption; + +/** + * Unbox {@link Model#options} or fallback to default for others. + * + * @internal + */ +export interface ModelTableOpts { + readonly idAttribute: IdAttribute; + readonly arrName: ArrName; + readonly mapName: MapName; + readonly fields: MClass['fields']; +} + +/** + * Handles the underlying data structure for a {@link Model} class. + */ +export class Table { + /** + * Creates a new {@link Table} instance. + * + * @param userOpts - options to use. + * @param [userOpts.idAttribute=DefaultTableOpts.idAttribute] - the id attribute of the entity. + * @param [userOpts.arrName=DefaultTableOpts.arrName] - the state attribute where an array of + * entity id's are stored + * @param [userOpts.mapName=DefaultTableOpts.mapName] - the state attribute where the entity objects + * are stored in a id to entity object + * map. + * @param [userOpts.fields=DefaultTableOpts.fields] - mapping of field key to {@link Field} object + */ + constructor(userOpts?: ModelTableOpts); + + getEmptyState(): TableState; +} + +/** + * Type of {@link Model} state's branch `meta` field. + */ +export interface DefaultMeta { + maxId: MIdType extends number ? number : null | number; +} + +export type TableIndexes = { + [K in FieldSpecKeys, OneToOne | ForeignKey>]: string +}; + +/** + * A mapped type parametrized by specific {@link Model} class. + * + * Infers actual state of the ORM branch based on the {@link Model} class provided. + */ +export type TableState = { + readonly meta: DefaultMeta>>; + readonly indexes: TableIndexes; +} & { readonly [K in ArrName]: ReadonlyArray>> } & + { + readonly [K in MapName]: { + readonly [K: string]: Ref>; + } + }; diff --git a/types/redux-orm/db/index.d.ts b/types/redux-orm/db/index.d.ts new file mode 100644 index 0000000000..57f2d5b617 --- /dev/null +++ b/types/redux-orm/db/index.d.ts @@ -0,0 +1,5 @@ +import createDatabase = require('./Database'); + +export * from './Database'; +export { ModelTableOpts, TableOpts, TableState, Table } from './Table'; +export default createDatabase; diff --git a/types/redux-orm/fields.d.ts b/types/redux-orm/fields.d.ts new file mode 100644 index 0000000000..300764159b --- /dev/null +++ b/types/redux-orm/fields.d.ts @@ -0,0 +1,73 @@ +export class Field { + readonly index: boolean; +} + +export interface AttributeOpts { + getDefault?: () => any; +} + +export class Attribute extends Field { + constructor(opts?: AttributeOpts); +} + +export interface AttributeWithDefault extends Attribute { + getDefault(): any; +} + +export interface RelationalFieldOpts { + to: string; + relatedName?: string; + through?: string; + throughFields?: { + to: string; + from: string; + }; + as?: string; +} + +export class RelationalField extends Field { + constructor(toModelName: string, relatedName?: string); + constructor(opts: RelationalFieldOpts); +} + +export class OneToOne extends RelationalField {} + +export class ForeignKey extends RelationalField { + readonly index: true; +} + +export class ManyToMany extends RelationalField { + readonly index: false; +} + +export interface AttrCreator { + (): Attribute; + (opts: AttributeOpts): AttributeWithDefault; +} + +export interface FkCreator { + (toModelName: string, relatedName?: string): ForeignKey; + (opts: RelationalFieldOpts): ForeignKey; +} + +export interface ManyCreator { + (toModelName: string, relatedName?: string): ManyToMany; + (opts: RelationalFieldOpts): ManyToMany; +} + +export interface OneToOneCreator { + (toModelName: string, relatedName?: string): OneToOne; + (opts: RelationalFieldOpts): OneToOne; +} + +export const attr: AttrCreator; + +export const oneToOne: OneToOneCreator; + +export const fk: FkCreator; + +export const many: ManyCreator; + +export interface FieldSpecMap { + [K: string]: Attribute | ForeignKey | ManyToMany | OneToOne; +} diff --git a/types/redux-orm/helpers.d.ts b/types/redux-orm/helpers.d.ts new file mode 100644 index 0000000000..1cb1bbd198 --- /dev/null +++ b/types/redux-orm/helpers.d.ts @@ -0,0 +1,23 @@ +/** + * Credits to Piotr Witek (http://piotrwitek.github.io) and utility-type project (https://github.com/piotrwitek/utility-types) + */ + +export type Assign & Intersection & Diff> = Pick< + I, + keyof I +>; + +export type Diff = Pick>; + +export type PickByValue = Pick; + +export type Overwrite & Intersection> = Pick; + +export type Optional = Omit & Partial>; + +export type Intersection = Pick< + T, + Extract & Extract +>; + +export type OptionalKeys = { [K in keyof T]-?: {} extends Pick ? K : never }[keyof T]; diff --git a/types/redux-orm/index.d.ts b/types/redux-orm/index.d.ts index a91f6cdba2..7f54641c50 100644 --- a/types/redux-orm/index.d.ts +++ b/types/redux-orm/index.d.ts @@ -1,215 +1,84 @@ -// Type definitions for redux-orm 0.9 +// Type definitions for redux-orm 0.13 // Project: https://github.com/redux-orm/redux-orm // Definitions by: Andrey Goncharov +// Tomasz Zabłocki // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.3 +// TypeScript Version: 3.5 -export interface ORMId { - id: string; -} +import { ORM, ORMOpts, OrmState } from './ORM'; +import Model, { + CreateProps, + CustomInstanceProps, + IdKey, + IdOrModelLike, + IdType, + ModelField, + ModelFieldMap, + ModelType, + Ref, + RefPropOrSimple, + SessionBoundModel, + UpdateProps, + UpsertProps +} from './Model'; +import QuerySet, { + LookupPredicate, + LookupProps, + LookupResult, + LookupSpec, + MutableQuerySet, + SortIteratee, + SortOrder +} from './QuerySet'; +import { OrmSession } from './Session'; +import { createDatabase, TableOpts, TableState } from './db'; +import { attr, Attribute, FieldSpecMap, fk, ForeignKey, many, ManyToMany, OneToOne, oneToOne } from './fields'; +import { createReducer, createSelector, defaultUpdater, ORMReducer, ORMSelector } from './redux'; -export interface TableState { - items: string[]; - itemsById: { [index: string]: Item }; - meta: Meta; -} +export { + FieldSpecMap, + LookupResult, + LookupSpec, + LookupPredicate, + LookupProps, + TableOpts, + RefPropOrSimple, + ModelFieldMap, + CustomInstanceProps, + UpsertProps, + CreateProps, + UpdateProps, + ModelField, + SortIteratee, + OrmSession as Session, + SortOrder, + MutableQuerySet, + createDatabase, + createSelector, + createReducer, + defaultUpdater, + ORMSelector, + ORMReducer, + IdOrModelLike, + Ref, + SessionBoundModel, + IdKey, + IdType, + ModelType, + ORM, + OrmState, + ORMOpts, + TableState, + Model, + QuerySet, + Attribute, + OneToOne, + ForeignKey, + ManyToMany, + attr, + oneToOne, + fk, + many +}; -export interface ORMCommonState { - [index: string]: TableState; -} - -export type SessionWithModels = Session & { [P in keyof State]: typeof Model }; - -export type ModelWithFields = Model & Fields & VirtualFields & Additional & ORMId; - -// TODO: Refine me -export type ModelProps = any; - -// TODO: Refine me -export interface DB { - getEmptyState: any; - query: any; - update: any; - describe: any; -} - -// TODO: Refine me -export interface SchemaSpec { - tables: any; -} - -// TODO: Refine me -export interface ORMOpts { - createDatabase: (schemaSpec: SchemaSpec) => any; -} - -export interface ModelFields { - [index: string]: Attribute | ForeignKey | ManyToMany | OneToOne; -} - -export interface ModelVirtualFields { - [index: string]: any; -} - -export class ORM { - constructor(opts?: ORMOpts) - register(...model: Array): void; - register(...model: Array): void; - registerManyToManyModelsFor(model: typeof Model): void; - get(modelName: string): typeof Model; - getModelClasses(): Array; - isFieldInstalled(modelName: string, fieldName: string): boolean; - setFieldInstalled(modelName: string, fieldName: string): void; - generateSchemaSpec(): SchemaSpec; - getDatabase(): DB; - getEmptyState(): State; - session(state: State): SessionWithModels; - mutableSession(state: State): SessionWithModels; - - private _attachQuerySetMethods(model: typeof Model): void; - private _setupModelPrototypes(models: Array): void; -} - -export class Model { - static readonly idAttribute: string; - static readonly session: SessionWithModels; - static readonly _sessionData: any; // TODO - static readonly query: QuerySet; - - static modelName: string; - static fields: ModelFields; - static virtualFields: ModelVirtualFields; - static querySetClass: typeof QuerySet; - - static toString(): string; - static options(): object; - static _getTableOpts(): object; - static markAccessed(): void; - static connec(session: Session): void; - static getQuerySet(): QuerySet; - static invalidateClassCache(): void; - static all(): QuerySet; - static create(props: Fields): ModelWithFields; - static upsert(props: Partial): ModelWithFields; - static withId(id: string): ModelWithFields; - static hasId(id: string): boolean; - static _findDatabaseRows(lookupObj: object): any; // TODO - static get(lookupObj: object): ModelWithFields; - static reducer(action: any, modelClass: typeof Model, session: SessionWithModels): any; - - readonly ref: Fields & Additional & ORMId; - - constructor(props: ModelProps) - - getClass(): string; - getId(): string; - toString(): string; - equals(otherModel: ModelWithFields): boolean; - set(propertyName: string, value: any): void; - update(userMergeObj: Partial): void; - refreshFromState(): void; - delete(): void; - - private _onDelete(): void; - private _initFields(props: ModelProps): void; - private _refreshMany2Many(relations: any): void; // TODO -} - -export type QuerySetClauses = any; // TODO -export type QuerySetOpts = any; // TODO -export class QuerySet { - static addSharedMethod(methodName: string): void; - - constructor(modelClass: typeof Model, clauses: QuerySetClauses, opts: QuerySetOpts) - - toString(): string; - toRefArray(): Array; - toModelArray(): Array>; - count(): number; - exists(): boolean; - at(index: string): ModelWithFields | undefined; - first(): ModelWithFields | undefined; - last(): ModelWithFields | undefined; - all(): QuerySet; - filter(lookupObj: object): QuerySet; // TODO - exclude(lookupObj: object): QuerySet; // TODO - orderBy(iteratees: any, orders: any): QuerySet; // TODO - update(mergeObj: Partial): void; - delete(): void; - - private _evaluate(): void; - private _new(clauses: QuerySetClauses, userOpts: QuerySetOpts): QuerySet; -} - -export class Session { - readonly accessedModels: string[]; - schema: ORM; - db: DB; - initialState: State; - withMutations: boolean; - batchToken: any; - sessionBoundModels: Array; - models: Array; - state: State; - - constructor(schema: ORM, db: DB, state: State, withMutations: boolean, batchToken: any) // TODO - - markAccessed(modelName: string): void; - getDataForModel(modelName: string): object; - applyUpdate(updateSpec: any): any; // TODO - query(querySpec: any): any; // TODO -} - -export interface AttributeOpts { - getDefault?: () => any; -} - -export class Attribute { - constructor(opts: AttributeOpts) - install(model: typeof Model, fieldName: string, orm: ORM): void; -} - -export interface RelationalFieldOpts { - to: string; - relatedName?: string; - through?: string; - throughFields?: { - to: string; - from: string; - }; -} - -export class RelationalField { - constructor(toModelName: string, relatedName?: string) - constructor(opts: RelationalFieldOpts) - getClass: typeof RelationalField; -} - -export class ForeignKey extends RelationalField { - install(model: typeof Model, fieldName: string, orm: ORM): void; -} - -export class ManyToMany extends RelationalField { - install(model: typeof Model, fieldName: string, orm: ORM): void; -} - -export class OneToOne extends RelationalField { - install(model: typeof Model, fieldName: string, orm: ORM): void; -} - -export function attr(opts?: AttributeOpts): Attribute; - -export function fk(toModelName: string, relatedName?: string): ForeignKey; -export function fk(opts: RelationalFieldOpts): ForeignKey; - -export function many(toModelName: string, relatedName?: string): ManyToMany; -export function oneToOne(toModelName: string, relatedName?: string): OneToOne; -export function oneToOne(opts: RelationalFieldOpts): OneToOne; - -export type Updater = (session: SessionWithModels, action: any) => any; - -export function createReducer(orm: ORM, updater?: Updater): (state: State, action: any) => State; - -export type ORMSelector = (session: SessionWithModels, ...args: any[]) => Result; - -export function createSelector(orm: ORM, ...args: Array>): (state: State) => any; +export default Model; diff --git a/types/redux-orm/redux-orm-tests.ts b/types/redux-orm/redux-orm-tests.ts index 045f4a3554..19086a7eb9 100644 --- a/types/redux-orm/redux-orm-tests.ts +++ b/types/redux-orm/redux-orm-tests.ts @@ -1,78 +1,374 @@ -import { attr, createSelector as createSelectorORM, ORMCommonState, ORMId, QuerySet, TableState, SessionWithModels, Model, ORM } from 'redux-orm'; +import { + attr, + createSelector as createOrmSelector, + fk, + IdKey, + IdType, + many, + Model, + ModelType, + MutableQuerySet, + ORM, + OrmState, + QuerySet, + Ref +} from 'redux-orm'; -// Model -export class Test extends Model { - static modelName = 'Test'; - - static fields = { - test: attr(), - isFetching: attr({ getDefault: () => false }), - id: attr() - }; +interface CreateBookAction { + type: 'CREATE_BOOK'; + payload: { coverArt?: string; title: string; publisher: number; authors?: string[] }; } -// core data which we do not have defaults for -export interface TestStateItem { - test: string; +interface DeleteBookAction { + type: 'DELETE_BOOK'; + payload: { title: string }; } -// optional data we provide defaults for -export interface FetchIndicatorState { - isFetching: boolean; +type RootAction = CreateBookAction | DeleteBookAction; + +interface BookFields { + title: string; + coverArt: string; + publisher: Publisher; + authors?: MutableQuerySet; } -// id attr is added automatically by redux-orm therefore we have IORMId interface -export type TestState = TableState; - -export interface TestORMState extends ORMCommonState { - Test: TestState; +class Book extends Model { + static modelName = 'Book' as const; + static fields = { + title: attr(), + coverArt: attr({ getDefault: () => 'empty.png' }), + publisher: fk('Publisher', 'books'), + authors: many({ to: 'Person', relatedName: 'books', through: 'Authorship' }) + }; + static options = { + idAttribute: 'title' as const + }; + static reducer(action: RootAction, Book: ModelType) { + switch (action.type) { + case 'CREATE_BOOK': + Book.create(action.payload); + break; + case 'DELETE_BOOK': + Book.filter(book => book.title === action.payload.title).delete(); + break; + default: + break; + } + } } -interface TestORMModels { - Test: typeof Test; +interface PersonFields { + id: string; + firstName: string; + lastName: string; + nationality?: string; + books?: MutableQuerySet; } -const orm = new ORM(); -orm.register(Test); - -// Reducer -interface TestDTO { - test: string; +class Person extends Model { + static modelName = 'Person' as const; + static fields = { + id: attr(), + firstName: attr(), + lastName: attr(), + nationality: attr() + }; } -interface Action

{ - type: string; - payload: P; +interface AuthorshipFields { + year?: number; + book: Book; + author: Person; } -const reducerAddItem = (state: TestORMState, action: Action): TestORMState => { - const session = orm.session(state); - session.Test.upsert(action.payload); - return session.state; +class Authorship extends Model { + static modelName = 'Authorship' as const; + static fields = { + year: attr(), + book: fk('Book'), + author: fk('Person') + }; +} + +interface PublisherFields { + index: number; + name: string; + books?: QuerySet; +} + +class Publisher extends Model { + static modelName = 'Publisher' as const; + static fields = { + index: attr(), + name: attr() + }; + static options = { + idAttribute: 'index' as const + }; +} + +const schema = { Book, Authorship, Person, Publisher }; + +type Schema = typeof schema; + +// create ORM instance and register { Book, Publisher, Person, Authorship } schema +const ormFixture = () => { + const orm = new ORM(); + orm.register(Book, Authorship, Person, Publisher); + return orm; }; -// Selector -interface TestDisplayItem { - test: string; -} -type TestDisplayItemList = TestDisplayItem[]; - -// Just for the example below. Use real createSelector from reselect in your app -const createSelector = (param1Creator: (state: S) => P1, combiner: (param1: P1) => R): (state: S) => R => (state) => - combiner(param1Creator(state)); - -interface RootState { - test: TestORMState; -} - -export const makeGetTestDisplayList = () => { - const ormSelector = createSelectorORM(orm, (session: SessionWithModels) => - (session.Test.all() as QuerySet) - .toRefArray() - .map((item) => ({ ...item })) - ); - return createSelector( - ({ test }) => test, - ormSelector - ); +// create ORM instance and acquire new session +const sessionFixture = () => { + const orm = ormFixture(); + return orm.session(orm.getEmptyState()); +}; + +// inferred optionality of ModelType.create argument properties +const argOptionalityAtModelCreation = () => { + const { Book, Publisher, Person } = sessionFixture(); + + /** + * 1.A. `number` Model identifiers are optional due to built-in incremental sequencing of numeric identifiers + * @see {@link PublisherFields.index} + */ + const publisher = Publisher.create({ name: 'P1' }); + + /** + * 1.B. `string` identifiers are mandatory + */ + const stringIdMissing = Book.create({ publisher: 1, coverArt: 'foo.bmp' }); // $ExpectError + + /** + * 2. non-relational fields with corresponding descriptors that contain defined `getDefault` callback: (`attr({ getDefault: () => 'empty.png' })`) + * @see {@link Book#fields.coverArt} + */ + const book2 = Book.create({ title: 'B2', publisher: 1 }); + + /** + * 3. both attribute and relational fields where corresponding ModelFields interface property has optional (`?`) modifier + * @see {@link BookFields.authors} + */ + const book1 = Book.create({ title: 'B1', publisher: 1, coverArt: 'foo.bmp' }); +}; + +// ModelFields contribute to type constraints within ModelType.create arguments +const argPropertyTypeRestrictionsOnCreate = () => { + const { Book, Publisher, Person } = sessionFixture(); + + /** Keys of declared model fields interface contribute strict requirements regarding corresponding property types */ + Book.create({ title: 'B1', publisher: 1, coverArt: 'foo.png', authors: ['A1'] }); + + /* Incompatible property types: */ + Book.create({ title: 1, publisher: 1 }); // $ExpectError + Book.create({ title: 'B1', publisher: 'P1' }); // $ExpectError + Book.create({ title: 'B1', publisher: 1, coverArt: 4 }); // $ExpectError + Book.create({ title: 'B1', publisher: 1, authors: {} }); // $ExpectError + Book.create({ title: 'B1', publisher: 1, authors: () => null }); // $ExpectError + + /** + * Properties associated to relational fields may be supplied with: + * + * - a primitive type matching id type of relation target + * - a Ref type derived from relation target + * - Model/SessionBoundModel instance matching relation target + * - a map containing {Idkey:IdType} entry, where IdKey/IdType are compatible with relation target id key:type signature + * + * In case of MutableQuerySets/many-to-many relationships, an array of union of above-mentioned types is accepted + */ + const authorModel = Person.create({ id: 'A1', firstName: 'A1', lastName: 'A1' }); + const publisherModel = Publisher.create({ name: 'P1' }); + Book.create({ title: 'B1', publisher: publisherModel, authors: [authorModel] }); + Book.create({ title: 'B1', publisher: publisherModel.ref, authors: [authorModel.ref] }); + Book.create({ + title: 'B1', + publisher: { index: publisherModel.index }, + authors: [{ id: authorModel.id }, 'A1', authorModel, authorModel.ref] + }); + + /** Id types are verified to match relation target */ + Book.create({ title: 'B1', publisher: authorModel }); // $ExpectError + Book.create({ title: 'B1', publisher: publisherModel.ref, authors: [publisherModel.ref, 'A1'] }); // $ExpectError + Book.create({ title: 'B1', publisher: { index: 'P1 ' } }); // $ExpectError + Book.create({ title: 'B1', publisher: { index: 0 }, authors: [authorModel, true] }); // $ExpectError +}; + +// ModelFields contribute to type constraints within ModelType.create arguments +const argPropertyTypeRestrictionsOnUpsert = () => { + const { Book, Publisher, Person } = sessionFixture(); + + /** Upsert requires id to be provided */ + Book.upsert({ publisher: 1 }); // $ExpectError + + // $ExpectType SessionBoundModel + Book.upsert({ title: 'B1', publisher: 1 }); + + /* Incompatible property types: */ + Book.upsert({ title: 4, publisher: 'P1' }); // $ExpectError + Book.upsert({ title: 'B1', publisher: 'P1' }); // $ExpectError + Book.upsert({ title: 'B1', publisher: 1, coverArt: 4 }); // $ExpectError + Book.upsert({ title: 'B1', publisher: 1, authors: {} }); // $ExpectError + Book.upsert({ title: 'B1', publisher: 1, authors: () => null }); // $ExpectError + + /** + * Properties associated to relational fields may be supplied with: + * + * - a primitive type matching id type of relation target + * - a Ref type derived from relation target + * - Model/SessionBoundModel instance matching relation target + * - a map containing {Idkey:IdType} entry, where IdKey/IdType are compatible with relation target id key:type signature + * + * In case of MutableQuerySets/many-to-many relationships, an array of union of above-mentioned types is accepted + */ + const authorModel = Person.upsert({ id: 'A1', firstName: 'A1', lastName: 'A1' }); + const publisherModel = Publisher.upsert({ name: 'P1', index: 1 }); + Book.upsert({ title: 'B1', publisher: publisherModel, authors: [authorModel] }); + Book.upsert({ title: 'B1', publisher: publisherModel.ref, authors: [authorModel.ref] }); + Book.upsert({ + title: 'B1', + publisher: { index: publisherModel.index }, + authors: [{ id: authorModel.id }, 'A1', authorModel, authorModel.ref] + }); + + /** Id types are verified to match relation target */ + Book.create({ title: 'B1', publisher: authorModel }); // $ExpectError + Book.create({ title: 'B1', publisher: publisherModel.ref, authors: [publisherModel.ref, 'A1'] }); // $ExpectError + Book.create({ title: 'B1', publisher: { index: 'P1 ' } }); // $ExpectError + Book.create({ title: 'B1', publisher: { index: 0 }, authors: [authorModel, true] }); // $ExpectError +}; + +// restriction of allowed ORM.register args +const restrictRegisterArgsToSchemaModels = () => { + const incompleteSchema = { Book, Authorship, Person }; + const orm = new ORM(); + orm.register(Book, Authorship, Person, Publisher); // $ExpectError +}; + +// inference of ORM branch state type +const inferOrmBranchEmptyState = () => { + const emptyState = ormFixture().getEmptyState(); + + const bookTableState = emptyState.Book; // $ExpectType TableState + const bookItemsById = emptyState.Book.itemsById; // $ExpectType { readonly [K: string]: Ref; } + const authorshipMetaState = emptyState.Authorship.meta.maxId; // $ExpectType number + const bookMetaState = emptyState.Book.meta.maxId; // $ExpectType number | null +}; + +// indexing session instance using registered Model.modelName returns narrowed Model class +const sessionInstanceExtendedWithNarrowedModelClasses = () => { + const { Book, Person, Publisher } = sessionFixture(); + + // $ExpectType { Book: ModelType; Person: ModelType; Publisher: ModelType; } + const sessionBoundModels = { Book, Person, Publisher }; +}; + +// IdKey and IdType mapped types support for valid identifier configurations +const idInferenceAndCustomizations = () => { + type ExtractId = [IdKey, IdType]; + + type ImplicitDefault = ExtractId; // $ExpectType ["id", number] + type CustomKey = ExtractId; // $ExpectType ["index", number] + type CustomType = ExtractId; // $ExpectType ["id", string] + type CustomKeyAndType = ExtractId; // $ExpectType ["title", string] +}; + +// Model#create result retains custom properties supplied during call +const customInstanceProperties = () => { + const { Book } = sessionFixture(); + + const basicBook = Book.create({ title: 'book', publisher: 1 }); + + type basicBookKeys = Exclude; // $ExpectType "title" | "coverArt" | "publisher" | "authors" + const basicBookTitle = basicBook.title; // $ExpectType string + const authors = basicBook.authors; // $ExpectType MutableQuerySet | undefined + const unknownPropertyError = basicBook.customProp; // $ExpectError + + const customProp = { foo: 0, bar: true }; + + const extendedBook = Book.create({ + title: 'extendedBook', + publisher: 1, + customProp + }); + + type customBookKeys = Exclude; // $ExpectType "title" | "coverArt" | "publisher" | "authors" | "customProp" + const extendedBookTitle = extendedBook.title; // $ExpectType string + const instanceCustomProp = extendedBook.customProp; // $ExpectType { foo: number; bar: boolean; } +}; + +// reducer API is intact +const standaloneReducerFunction = () => { + const orm = ormFixture(); + + type StateType = OrmState; + + const reducerAddItem = (state: StateType, action: CreateBookAction): StateType => { + const session = orm.session(state); + session.Book.create(action.payload); + return session.state; + }; +}; + +// QuerySet type is retained though query chain until terminated. +// Orders are optional, must conform to SortOrder type when present. +// QuerySet.orderBy overloads accept iteratees applicable to QuerySet's type only +const orderByArguments = () => { + const { Book } = sessionFixture(); + const booksQuerySet = Book.all(); + + // $ExpectType readonly Ref[] + const singleIteratee = booksQuerySet + .orderBy('title') + .orderBy(book => book.publisher, 'desc') + .orderBy(book => book.title, false) + .orderBy('publisher', 'asc') + .orderBy('publisher', true) + .toRefArray(); + + // $ExpectType readonly Ref[] + const arrayIteratee = booksQuerySet + .orderBy(['title'], ['asc']) + .orderBy(['publisher', 'title'], [true, 'desc']) + .orderBy([book => book.title], ['desc']) + .orderBy(['title']) + .orderBy([book => book.title, 'publisher'], ['desc', false]) + .toRefArray(); + + const invalidSingleKeyIteratee = booksQuerySet.orderBy('notABookPropertyKey'); // $ExpectError + const invalidSingleFunctionIteratee = booksQuerySet.orderBy([book => book.notABookPropertyKey], false); // $ExpectError + const invalidStringOrderDirectionType = booksQuerySet.orderBy('title', 'inc'); // $ExpectError + const invalidSingleOrderDirectionType = booksQuerySet.orderBy('title', 4); // $ExpectError + const invalidArrayKeyIteratee = booksQuerySet.orderBy(['notABookPropertyKey']); // $ExpectError + const invalidArrayFunctionIteratee = booksQuerySet.orderBy([book => book.notABookPropertyKey]); // $ExpectError + const invalidArrayStringOrderDirection = booksQuerySet.orderBy(['title'], ['inc']); // $ExpectError + const invalidArrayOrderDirectionType = booksQuerySet.orderBy(['title'], [4]); // $ExpectError +}; + +const selectors = () => { + // test fixture, use reselect.createSelector in production code + const createSelector = , Result extends any>( + param1Creator: (state: S) => OS, + combiner: (param1: OS) => Result + ): ((state: S) => Result) => state => combiner(param1Creator(state)); + + const orm = ormFixture(); + + const ormSelector = createOrmSelector(orm, session => session.Book.all().toRefArray()[0]); + + interface RootState { + db: OrmState; + } + + const selector = createSelector, Ref>( + ({ db }) => db, + ormSelector + ); + + createSelector, Ref>( + ({ db }) => db, + ormSelector // $ExpectError + ); + + selector({ db: orm.getEmptyState() }); // $ExpectType Ref }; diff --git a/types/redux-orm/redux.d.ts b/types/redux-orm/redux.d.ts new file mode 100644 index 0000000000..a8f1ba6e8c --- /dev/null +++ b/types/redux-orm/redux.d.ts @@ -0,0 +1,25 @@ +import { IndexedModelClasses, ORM, OrmState } from './ORM'; +import { OrmSession } from './Session'; + +export interface ORMReducer, TAction extends any = any> { + (state: OrmState | undefined, action: TAction): OrmSession; +} + +export type defaultUpdater, TAction extends any = any> = ( + session: OrmSession, + action: TAction +) => void; + +export function createReducer, TAction extends any = any>( + orm: ORM, + updater?: defaultUpdater +): ORMReducer; + +export interface ORMSelector, Result extends any = any> { + (session: OrmSession, ...args: any[]): Result; +} + +export function createSelector, Result extends any = any>( + orm: ORM, + ormSelector: ORMSelector +): (state: OrmState) => Result; diff --git a/types/redux-orm/tsconfig.json b/types/redux-orm/tsconfig.json index f981bceb20..d65d13867d 100644 --- a/types/redux-orm/tsconfig.json +++ b/types/redux-orm/tsconfig.json @@ -1,23 +1,30 @@ { "compilerOptions": { "module": "commonjs", - "lib": [ - "es6" - ], + "lib": ["es6"], + "target": "es6", "noImplicitAny": true, "noImplicitThis": true, "strictNullChecks": true, "strictFunctionTypes": true, "baseUrl": "../", - "typeRoots": [ - "../" - ], + "typeRoots": ["../"], "types": [], "noEmit": true, "forceConsistentCasingInFileNames": true }, "files": [ "index.d.ts", - "redux-orm-tests.ts" + "redux-orm-tests.ts", + "db/index.d.ts", + "db/Database.d.ts", + "db/Table.d.ts", + "helpers.d.ts", + "redux.d.ts", + "fields.d.ts", + "ORM.d.ts", + "Model.d.ts", + "QuerySet.d.ts", + "Session.d.ts" ] }