From 408adf638f27175c4a7e7d6fe356a2f3fe1fa243 Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Mon, 24 Oct 2016 00:00:55 -0700 Subject: [PATCH] feat(docs): Initial pass at API references for gh-pages --- lib/attributes/attribute-collection.js | 8 +- lib/attributes/attribute-joined-data.js | 4 +- lib/attributes/attribute-object.js | 2 +- lib/attributes/attribute-string.js | 4 +- lib/attributes/matcher.js | 36 ++-- lib/attributes/sort-order.js | 13 +- lib/database-change-record.js | 11 +- lib/database-setup-query-builder.js | 7 +- lib/database-store.js | 234 +++++++++++++++--------- lib/database-transaction.js | 10 + lib/model.js | 7 +- lib/query-range.js | 6 +- lib/query-result-set.js | 64 +++++-- lib/query-subscription-pool.js | 4 +- lib/query.js | 26 +-- 15 files changed, 292 insertions(+), 144 deletions(-) diff --git a/lib/attributes/attribute-collection.js b/lib/attributes/attribute-collection.js index b1756a0..6a2f636 100644 --- a/lib/attributes/attribute-collection.js +++ b/lib/attributes/attribute-collection.js @@ -1,8 +1,8 @@ import Attribute from './attribute'; import Matcher from './matcher'; -/* -Public: Collection attributes provide basic support for one-to-many relationships. +/** +Collection attributes provide basic support for one-to-many relationships. For example, Threads in N1 have a collection of Labels or Folders. When Collection attributes are marked as `queryable`, the DatabaseStore @@ -92,7 +92,9 @@ export default class AttributeCollection extends Attribute { return objs; } - // Public: Returns a {Matcher} for objects containing the provided value. + /** + @returns {Matcher} - Matcher for objects containing the provided value. + */ contains(val) { this._assertPresentAndQueryable('contains', val); return new Matcher(this, 'contains', val); diff --git a/lib/attributes/attribute-joined-data.js b/lib/attributes/attribute-joined-data.js index 669b2b9..98c16e3 100644 --- a/lib/attributes/attribute-joined-data.js +++ b/lib/attributes/attribute-joined-data.js @@ -2,8 +2,8 @@ import Attribute from './attribute'; const NullPlaceholder = "!NULLVALUE!"; -/* -Public: Joined Data attributes allow you to store certain attributes of an +/** +Joined Data attributes allow you to store certain attributes of an object in a separate table in the database. We use this attribute type for Message bodies. Storing message bodies, which can be very large, in a separate table allows us to make queries on message diff --git a/lib/attributes/attribute-object.js b/lib/attributes/attribute-object.js index e088055..05c6edb 100644 --- a/lib/attributes/attribute-object.js +++ b/lib/attributes/attribute-object.js @@ -1,7 +1,7 @@ import Attribute from './attribute'; /** -An object that can be cast to `itemClass` +The value of this attribute is always an object that can be cast to `itemClass` */ export default class AttributeObject extends Attribute { constructor({modelKey, jsonKey, itemClass, queryable}) { diff --git a/lib/attributes/attribute-string.js b/lib/attributes/attribute-string.js index 3537a40..3e671f6 100644 --- a/lib/attributes/attribute-string.js +++ b/lib/attributes/attribute-string.js @@ -4,8 +4,8 @@ import Matcher from './matcher'; /** The value of this attribute is always a string or `null`. -String attributes can be queries using `equal`, `not`, and `startsWith`. Matching on -`greaterThan` and `lessThan` is not supported. +String attributes can be queries using `equal`, `not`, and `startsWith`. +Matching on `greaterThan` and `lessThan` is not supported. */ export default class AttributeString extends Attribute { toJSON(val) { diff --git a/lib/attributes/matcher.js b/lib/attributes/matcher.js index e2771f7..b5811d2 100644 --- a/lib/attributes/matcher.js +++ b/lib/attributes/matcher.js @@ -8,37 +8,37 @@ const singleQuoteEscapeSequence = "''"; const doubleQuoteEscapeSequence = '""'; -/* -Public: The Matcher class encapsulates a particular comparison clause on an {Attribute}. +/** +The Matcher class encapsulates a particular comparison clause on an {@link Attribute}. Matchers can evaluate whether or not an object matches them, and also compose -SQL clauses for the DatabaseStore. Each matcher has a reference to a model -attribute, a comparator and a value. +SQL clauses for the {@link DatabaseStore}. Each matcher has a reference to a model +attribute, a comparator and a value. This class is heavily inspired by +NSPredicate on Mac OS X / CoreData. -```coffee +```js // Retrieving Matchers -isUnread = Thread.attributes.unread.equal(true) +const isUnread = Thread.attributes.unread.equal(true); -hasLabel = Thread.attributes.categories.contains('label-id-123') +const hasLabel = Thread.attributes.categories.contains('label-id-123'); // Using Matchers in Database Queries -db.findAll(Thread).where(isUnread)... +const db.findAll(Thread).where(isUnread)... // Using Matchers to test Models -threadA = new Thread(unread: true) -threadB = new Thread(unread: false) +const threadA = new Thread({unread: true}) +const threadB = new Thread({unread: false}) isUnread.evaluate(threadA) // => true + isUnread.evaluate(threadB) // => false ``` - -Section: Database */ class Matcher { constructor(attr, comparator, val) { @@ -161,6 +161,10 @@ class Matcher { Matcher.muid = 0 +/** +This subclass is publicly exposed as Matcher.Or. +@private +*/ class OrCompositeMatcher extends Matcher { constructor(children) { super(); @@ -196,6 +200,10 @@ class OrCompositeMatcher extends Matcher { } } +/** +This subclass is publicly exposed as Matcher.And. +@private +*/ class AndCompositeMatcher extends Matcher { constructor(children) { super(); @@ -231,6 +239,10 @@ class AndCompositeMatcher extends Matcher { } } +/** +This subclass is publicly exposed as Matcher.Not. +@private +*/ class NotCompositeMatcher extends AndCompositeMatcher { whereSQL(klass) { return `NOT (${super.whereSQL(klass)})`; diff --git a/lib/attributes/sort-order.js b/lib/attributes/sort-order.js index dd1ee38..9cafff9 100644 --- a/lib/attributes/sort-order.js +++ b/lib/attributes/sort-order.js @@ -1,13 +1,14 @@ -/* -Public: Represents a particular sort direction on a particular column. You should not -instantiate SortOrders manually. Instead, call {Attribute::ascending} or -{Attribute::descending} to obtain a sort order instance: +/** +Represents a particular sort direction on a particular column. You should not +instantiate SortOrders manually. Instead, call {@link Attribute.ascending} or +{@link Attribute.descending} to obtain a sort order instance: -```coffee +```js db.findBy(Message) .where({threadId: threadId, draft: false}) - .order(Message.attributes.date.descending()).then (messages) -> + .order(Message.attributes.date.descending()).then((messages) { +}); ``` Section: Database diff --git a/lib/database-change-record.js b/lib/database-change-record.js index 1a92142..10468de 100644 --- a/lib/database-change-record.js +++ b/lib/database-change-record.js @@ -1,10 +1,13 @@ import {registeredObjectReplacer, registeredObjectReviver} from './utils'; /** -DatabaseChangeRecord is the object emitted from the DatabaseStore when it triggers. -The DatabaseChangeRecord contains information about what type of model changed, -and references to the new model values. All mutations to the database produce these -change records. +An RxDB database emits DatabaseChangeRecord objects once a transaction has completed. +The change record contains a copy of the model(s) that were modified, the type of +modification (persist / destroy) and the model class. + +DatabaseChangeRecords can be serialized to JSON and RxDB transparently bridges +them between windows of Electron applications. Change records of the same type +and model class can also be merged. */ export default class DatabaseChangeRecord { diff --git a/lib/database-setup-query-builder.js b/lib/database-setup-query-builder.js index 0359460..c219279 100644 --- a/lib/database-setup-query-builder.js +++ b/lib/database-setup-query-builder.js @@ -6,9 +6,10 @@ import Attributes from './attributes'; const {AttributeCollection, AttributeJoinedData} = Attributes; /** -The DatabaseConnection dispatches queries to the Browser process via IPC and listens -for results. It maintains a hash of `_queryRecords` representing queries that are -currently running and fires promise callbacks when complete. +The factory methods in this class assemble SQL queries that build Model +tables based on their attribute schema. + +@private */ export default class DatabaseSetupQueryBuilder { diff --git a/lib/database-store.js b/lib/database-store.js index 4a44a96..28a0a13 100644 --- a/lib/database-store.js +++ b/lib/database-store.js @@ -28,52 +28,18 @@ class IncorrectVersionError extends Error { } } /** -N1 is built on top of a custom database layer modeled after -ActiveRecord. For many parts of the application, the database is the source -of truth. Data is retrieved from the API, written to the database, and changes -to the database trigger Stores and components to refresh their contents. +The DatabaseStore is the central database object of RxDB. You can instantiate +as many databases as you'd like at the same time, and opening the same +database path in multiple windows is fine - RxDB uses SQLite transactions and +dispatches change events across windows via the Electron IPC module. -The DatabaseStore is available in every application window and allows you to -make queries against the local cache. Every change to the local cache is -broadcast as a change event, and listening to the DatabaseStore keeps the -rest of the application in sync. +This class extends EventEmitter, and you can subscribe to all changes to the +database by subscribing to the `trigger` event. -## Listening for Changes +For more information about getting started with RxDB, see the Getting Started +guide. -To listen for changes to the local cache, subscribe to the DatabaseStore and -inspect the changes that are sent to your listener method. - -```js -this.unsubscribe = db.listen(this._onDataChanged, this) - -... - -_onDataChanged(change) { - if (change.objectClass !== Message) { - return; - } - if (!change.objects.map((m) => m.id).includes(this._myMessageID)) { - return; - } - // Refresh Data -} - -``` - -The local cache changes very frequently, and your stores and components should -carefully choose when to refresh their data. The \`change\` object passed to your -event handler allows you to decide whether to refresh your data and exposes -the following keys: - - - `objectClass`: The {Model} class that has been changed. If multiple types of models - were saved to the database, you will receive multiple change events. - - - `objects`: An {Array} of {Model} instances that were either created, updated or - deleted from the local cache. If your component or store presents a single object - or a small collection of objects, you should look to see if any of the objects - are in your displayed set before refreshing. - -Section: Database +@extends EventEmitter */ export default class DatabaseStore extends EventEmitter { @@ -117,6 +83,11 @@ export default class DatabaseStore extends EventEmitter { ipcRenderer.on('database-trigger', this._onIPCTrigger); } + /** + Typically, instances of DatabaseStore are long-lasting and are created in + renderer processes when they load. If you need to manually tear down an instance + of DatabaseStore, call `disconnect`. + */ disconnect() { ipcRenderer.removeListener('database-phase-change', this._onPhaseChange); ipcRenderer.removeListener('database-trigger', this._onIPCTrigger); @@ -230,6 +201,11 @@ export default class DatabaseStore extends EventEmitter { this._db.pragma(`user_version=${this._options.databaseVersion}`); + /** + @event DatabaseStore#did-setup-database + @type {object} + @property {object} sqlite - The underlying SQLite3 database instance. + */ this.emit('did-setup-database', {sqlite: this._db}); return ready(); @@ -253,15 +229,28 @@ export default class DatabaseStore extends EventEmitter { } _handleSetupError(error = (new Error(`Manually called _handleSetupError`))) { + /** + @event DatabaseStore#will-rebuild-database + @type {object} + @property {object} sqlite - The underlying SQLite3 database instance. + @property {Error} error - The error that occurred. + */ this.emit('will-rebuild-database', {sqlite: this._db, error: error}); coordinator.rebuildDatabase(); } - // Returns a Promise that resolves when the query has been completed and - // rejects when the query has failed. - // - // If a query is made before the database has been opened, the query will be - // held in a queue and run / resolved when the database is ready. + /** + Executes a SQL string on the database. If a query is made before the database + has been opened, the query will be held in a queue and run / resolved when + the database is ready. + + @protected + + @param {String} query - A SQLite SQL string + @param {Array} values - An array of values, corresponding to `?` in the SQL string. + @returns {Promise} - Resolves when the query has been completed and rejects when + the query has failed. + */ _query(query, values = []) { return new Promise((resolve, reject) => { if (!this._open) { @@ -345,16 +334,18 @@ export default class DatabaseStore extends EventEmitter { // ActiveRecord-style Querying /** - Creates a new Model Query for retrieving a single model specified by + Creates a new Query for retrieving a single model specified by the class and id. - @param {Model} class - The class of the {Model} you're trying to retrieve. + @param {Model} klass - The class of the {Model} you're trying to retrieve. @param {String} id - The id of the {Model} you're trying to retrieve Example: - ```coffee - db.find(Thread, 'id-123').then (thread) -> + + ```js + db.find(Thread, 'id-123').then((thread) => { // thread is a Thread object, or null if no match was found. + }); ``` @returns {Query} @@ -373,9 +364,8 @@ export default class DatabaseStore extends EventEmitter { Creates a new Model Query for retrieving a single model matching the predicates provided. - @param {Model} class - The class of the {Model} you're trying to retrieve. - @param {Array} predicates - An {Array} of {matcher} objects. The set of predicates the - returned model must match. + @param {Model} klass - The class of the {Model} you're trying to retrieve. + @param {Matcher[]} predicates - the set of predicates the returned model must match. @returns {Query} */ @@ -390,9 +380,9 @@ export default class DatabaseStore extends EventEmitter { Creates a new Model Query for retrieving all models matching the predicates provided. - @param {Model} class - The class of the {Model} you're trying to retrieve. - @param {Array} predicates - An {Array} of {matcher} objects. The set of predicates the - returned model must match. + @param {Model} klass - The class you're trying to retrieve. + @param {Matcher[]} predicates - An array of matcher objects. The set of + predicates the returned model must match. @returns {Query} */ @@ -404,12 +394,12 @@ export default class DatabaseStore extends EventEmitter { } /** - Public: Creates a new Model Query that returns the {Number} of models matching + Creates a new Query that returns the number of models matching the predicates provided. - @param {Model} class - The class of the {Model} you're trying to retrieve. - @param {Array} predicates - An {Array} of {matcher} objects. The set of predicates the - returned model must match. + @param {Model} klass - The Model class you're trying to retrieve. + @param {Matcher[]} predicates - The set of predicates the returned model + must match. @returns {Query} */ @@ -421,13 +411,16 @@ export default class DatabaseStore extends EventEmitter { } /** - Public: Modelify converts the provided array of IDs or models (or a mix of - IDs and models) into an array of models of the \`klass\` provided by querying for the missing items. + Modelify takes a mixed array of model IDs or model instances, and + queries for items that are missing. The returned array contains just model + instances, or null if the model could not be found. + + This function is useful if your code may receive an item or it's ID. Modelify is efficient and uses a single database query. It resolves Immediately - if no query is necessary. + if no query is necessary. It does not change the order of items in the array. - @param {Model} class - The model class desired + @param {Model} klass - The model class desired @param {Array} arr - An {Array} with a mix of string model IDs and/or models. @returns {Promise} - A promise that resolves with the models. @@ -463,7 +456,10 @@ export default class DatabaseStore extends EventEmitter { } /** - Executes a {Query} on the local database. + Executes a model {Query} on the local database. Typically, this method is + called transparently and you do not need to invoke it directly. + + @protected @param {Query} modelQuery - The query to execute. @@ -483,18 +479,24 @@ export default class DatabaseStore extends EventEmitter { return new JSONBlob.Query(JSONBlob, this).where({id}).one(); } - // Private: Mutation hooks allow you to observe changes to the database and - // add additional functionality before and after the REPLACE / INSERT queries. - // - // beforeDatabaseChange: Run queries, etc. and return a promise. The DatabaseStore - // will proceed with changes once your promise has finished. You cannot call - // persistModel or unpersistModel from this hook. - // - // afterDatabaseChange: Run queries, etc. after the REPLACE / INSERT queries - // - // Warning: this is very low level. If you just want to watch for changes, You - // should subscribe to the DatabaseStore's trigger events. - // + /** + Mutation hooks allow you to observe changes to the database and + add functionality within the transaction, before and/or after the standard + REPLACE / INSERT queries are made. + + - beforeDatabaseChange: Run queries, etc. and return a promise. The DatabaseStore + will proceed with changes once your promise has finished. You cannot call + persistModel or unpersistModel from this hook. Instead, use low level calls + like DatabaseStore._query. + + - afterDatabaseChange: Run queries, etc. after the `REPLACE` / `INSERT` queries + + Warning: this is very low level. If you just want to watch for changes, You + should subscribe to the DatabaseStore's trigger events. + + Example: N1 uses these hooks to watch for changes to unread counts, which are + maintained in a separate table to avoid frequent `COUNT(*)` queries. + */ addMutationHook({beforeDatabaseChange, afterDatabaseChange}) { if (!beforeDatabaseChange) { throw new Error(`DatabaseStore:addMutationHook - You must provide a beforeDatabaseChange function`); @@ -505,23 +507,35 @@ export default class DatabaseStore extends EventEmitter { this._mutationHooks.push({beforeDatabaseChange, afterDatabaseChange}); } + /** + Removes a previously registered mutation hook. You must pass the exact + same object that was provided to {DatabaseStore.addMutationHook}. + */ removeMutationHook(hook) { this._mutationHooks = this._mutationHooks.filter(h => h !== hook); } + /** + @returns currently registered mutation hooks + */ mutationHooks() { return this._mutationHooks; } /** - Opens a new database transaction for writing changes. - inTransacion makes the following guarantees: + Opens a new database transaction and executes the provided `fn` within the + transaction. After the transaction function resolves, the transaction is + closed and changes are relayed to live queries and other subscribers. + + RxDB makes the following guaruntees: - - No other calls to \`inTransaction\` will run until the promise has finished. + - Serial Execution: Once started, no other calls to `inTransaction` will + excute until the promise returned by `fn` has finished. - - No other process will be able to write to sqlite while the provided function - is running. `BEGIN IMMEDIATE TRANSACTION` semantics are: + - Single Process Writing: No other process will be able to write to the + database while the provided function is running. RxDB uses SQLite's + `BEGIN IMMEDIATE TRANSACTION`, with the following semantics: + No other connection will be able to write any changes. + Other connections can read from the database, but they will not see pending changes. @@ -531,6 +545,8 @@ export default class DatabaseStore extends EventEmitter { @returns {Promise} - A promise that resolves when the transaction has successfully completed. + + @emits DatabaseStore#trigger **/ inTransaction(fn) { return this._transactionQueue.add(() => @@ -538,6 +554,9 @@ export default class DatabaseStore extends EventEmitter { ); } + /** + @protected + */ transactionDidCommitChanges(changeRecords) { for (const record of changeRecords) { this._debouncer.accumulate(record); @@ -546,17 +565,26 @@ export default class DatabaseStore extends EventEmitter { // Search Index Operations + /** + @protected + */ createSearchIndex(klass) { const sql = this._queryBuilder.createSearchIndexSql(klass); return this._query(sql); } + /** + @protected + */ searchIndexSize(klass) { const searchTableName = `${klass.name}Search`; const sql = `SELECT COUNT(content_id) as count FROM \`${searchTableName}\``; return this._query(sql).then((result) => result[0].count); } + /** + @protected + */ isIndexEmptyForAccount(accountId, modelKlass) { const modelTable = modelKlass.name const searchTable = `${modelTable}Search` @@ -568,6 +596,9 @@ export default class DatabaseStore extends EventEmitter { return this._query(sql, [accountId]).then(result => result.length === 0); } + /** + @protected + */ dropSearchIndex(klass) { if (!klass) { throw new Error(`DatabaseStore::createSearchIndex - You must provide a class`); @@ -577,6 +608,9 @@ export default class DatabaseStore extends EventEmitter { return this._query(sql); } + /** + @protected + */ isModelIndexed(model, isIndexed) { if (isIndexed === true) { return Promise.resolve(true); @@ -590,6 +624,9 @@ export default class DatabaseStore extends EventEmitter { ) } + /** + @protected + */ indexModel(model, indexData, isModelIndexed) { const searchTableName = `${model.constructor.name}Search`; return this.isModelIndexed(model, isModelIndexed).then((isIndexed) => { @@ -608,6 +645,9 @@ export default class DatabaseStore extends EventEmitter { }); } + /** + @protected + */ updateModelIndex(model, indexData, isModelIndexed) { const searchTableName = `${model.constructor.name}Search`; this.isModelIndexed(model, isModelIndexed).then((isIndexed) => { @@ -629,6 +669,9 @@ export default class DatabaseStore extends EventEmitter { }); } + /** + @protected + */ unindexModel(model) { const searchTableName = `${model.constructor.name}Search`; const sql = ( @@ -637,6 +680,9 @@ export default class DatabaseStore extends EventEmitter { return this._query(sql, [model.id]); } + /** + @protected + */ unindexModelsForAccount(accountId, modelKlass) { const modelTable = modelKlass.name; const searchTableName = `${modelTable}Search`; @@ -649,6 +695,19 @@ export default class DatabaseStore extends EventEmitter { // Compatibility with Reflux / Flux Stores + /** + For compatibility with Reflux, Flux and other libraries, you can subscribe to + the database using `listen`: + + ```js + componentDidMount() { + this._unsubscribe = db.listen(this._onDataChanged, this); + } + ``` + + @param {Function} callback - The function to execute when the database triggers. + @param {Object} [bindContext] - Optional binding for `callback`. + */ listen(callback, bindContext = this) { if (!callback) { throw new Error("DatabaseStore.listen called with undefined callback"); @@ -665,11 +724,18 @@ export default class DatabaseStore extends EventEmitter { } } + /** + @protected + */ trigger(record) { ipcRenderer.send('database-trigger', { path: this._options.databasePath, json: record.toJSON(), }); + /** + @event DatabaseStore#trigger + @type {DatabaseChangeRecord} + */ this.emit('trigger', record); } } diff --git a/lib/database-transaction.js b/lib/database-transaction.js index 55a8101..2e278b5 100644 --- a/lib/database-transaction.js +++ b/lib/database-transaction.js @@ -11,6 +11,13 @@ require('promise.try').shim() const {AttributeCollection, AttributeJoinedData} = Attributes; +/** +DatabaseTransaction exposes a convenient API for querying and modifying an RxDB +within a SQLite transaction. + +You shouldn't need to instantiate this class directly. Instead, use +DatabaseStore#inTransaction. +*/ export default class DatabaseTransaction { constructor(database) { this.database = database; @@ -18,6 +25,9 @@ export default class DatabaseTransaction { this._opened = false; } + /** + @borrows DatabaseStore#find + */ find(...args) { return this.database.find(...args) } findBy(...args) { return this.database.findBy(...args) } findAll(...args) { return this.database.findAll(...args) } diff --git a/lib/model.js b/lib/model.js index bf860cc..9dc4aae 100644 --- a/lib/model.js +++ b/lib/model.js @@ -5,11 +5,14 @@ function generateTempId() { return `local-${s4()}${s4()}-${s4()}`; } /** -Public: A base class for API objects that provides abstract support for -serialization and deserialization, matching by attributes, and ID-based equality. +A base class for RxDB models that provides abstract support for JSON +serialization and deserialization, and attribute-based matching. + +Your RxDB data classes should extend Model and extend it's attributes: - {AttributeString} id: The resolved canonical ID of the model used in the database and generally throughout the app. + */ export default class Model { diff --git a/lib/query-range.js b/lib/query-range.js index 0ba403a..2031f1d 100644 --- a/lib/query-range.js +++ b/lib/query-range.js @@ -1,4 +1,8 @@ - +/** +QueryRange represents a LIMIT + OFFSET pair and provides high-level methods +for comparing, copying and joining ranges. It also provides syntax sugar +around infinity (the absence of a defined LIMIT or OFFSET). +*/ export default class QueryRange { static infinite() { return new QueryRange({limit: null, offset: null}); diff --git a/lib/query-result-set.js b/lib/query-result-set.js index 442f782..a1499e9 100644 --- a/lib/query-result-set.js +++ b/lib/query-result-set.js @@ -1,8 +1,8 @@ import QueryRange from './query-range'; /** -Public: Instances of QueryResultSet hold a set of models retrieved -from the database at a given offset. +Instances of QueryResultSet hold a set of models retrieved from the database +for a given query and offset. Complete vs Incomplete: @@ -13,14 +13,10 @@ has every model. Offset vs Index: -To avoid confusion, "index" refers to an item's position in an -array, and "offset" refers to it's position in the query result set. For example, -an item might be at index 20 in the _ids array, but at offset 120 in the result. - -Ids and ids: - -QueryResultSet calways returns object `ids` when asked for ids, but lookups -for models by id work once models are loaded. +To avoid confusion, "index" (used within the implementation) refers to an item's +position in an array, and "offset" refers to it's position in the query +result set. For example, an item might be at index 20 in the _ids array, but +at offset 120 in the result. */ export default class QueryResultSet { @@ -53,42 +49,78 @@ export default class QueryResultSet { }); } + /** + @returns {Boolean} - True if every model in the result set is available, false + if part of the set is still being loaded. (Usually following range changes.) + */ isComplete() { return this._ids.every((id) => this._modelsHash[id]); } + /** + @returns {QueryRange} - The represented range. + */ range() { return new QueryRange({offset: this._offset, limit: this._ids.length}); } + /** + @returns {Query} - The represented query. + */ query() { return this._query; } + /** + @returns {QueryRange} - The number of items in the represented range. Note + that a range (`LIMIT 10 OFFSET 0`) may return fewer than the maximum number + of items if none match the query. + */ count() { return this._ids.length; } + /** + @returns {Boolean} - True if the result set is empty, false otherwise. + */ empty() { return this.count() === 0; } + /** + @returns {Array} - the model IDs in the represented result. + */ ids() { return this._ids; } + /** + @param {Number} offset - The desired offset. + @returns {Array} - the model ID available at the requested offset, or undefined. + */ idAtOffset(offset) { return this._ids[offset - this._offset]; } + /** + @returns {Array} - An array of model objects. If the result is not yet complete, + this array may contain `undefined` values. + */ models() { return this._ids.map((id) => this._modelsHash[id]); } + /** + @protected + */ modelCacheCount() { return Object.keys(this._modelsHash).length; } + /** + @param {Number} offset - The desired offset. + @returns {Model} - the model at the requested offset. + */ modelAtOffset(offset) { if (!Number.isInteger(offset)) { throw new Error("QueryResultSet.modelAtOffset() takes a numeric index. Maybe you meant modelWithId()?"); @@ -96,11 +128,15 @@ export default class QueryResultSet { return this._modelsHash[this._ids[offset - this._offset]]; } + /** + @param {String} id - The desired ID. + @returns {Model} - the model with the requested ID, or undefined. + */ modelWithId(id) { return this._modelsHash[id]; } - buildIdToIndexHash() { + _buildIdToIndexHash() { this._idToIndexHash = {} this._ids.forEach((id, idx) => { this._idToIndexHash[id] = idx; @@ -111,9 +147,13 @@ export default class QueryResultSet { }); } + /** + @param {String} id - The desired ID. + @returns {Number} - the offset of `ID`, relative to all of the query results. + */ offsetOfId(id) { if (this._idToIndexHash === null) { - this.buildIdToIndexHash(); + this._buildIdToIndexHash(); } if (this._idToIndexHash[id] === undefined) { diff --git a/lib/query-subscription-pool.js b/lib/query-subscription-pool.js index 18665ce..3519349 100644 --- a/lib/query-subscription-pool.js +++ b/lib/query-subscription-pool.js @@ -2,9 +2,11 @@ import QuerySubscription from './query-subscription'; /** -Public: The QuerySubscriptionPool maintains a list of all of the query +The QuerySubscriptionPool maintains a list of all of the query subscriptions in the app. In the future, this class will monitor performance, merge equivalent subscriptions, etc. + +@protected */ export default class QuerySubscriptionPool { constructor(database) { diff --git a/lib/query.js b/lib/query.js index 52491a4..ab034b9 100644 --- a/lib/query.js +++ b/lib/query.js @@ -8,35 +8,36 @@ import {registeredObjectReviver, tableNameForJoin} from './utils'; const {Matcher, AttributeJoinedData, AttributeCollection} = Attributes; /** -Public: ModelQuery exposes an ActiveRecord-style syntax for building database queries +ModelQuery exposes an ActiveRecord-style syntax for building database queries that return models and model counts. Model queries are returned from the factory methods -{DatabaseStore::find}, {DatabaseStore::findBy}, {DatabaseStore::findAll}, and {DatabaseStore::count}, and are the primary interface for retrieving data +{DatabaseStore::find}, {DatabaseStore::findBy}, {DatabaseStore::findAll}, +and {DatabaseStore::count}, and are the primary interface for retrieving data from the app's local cache. ModelQuery does not allow you to modify the local cache. To create, update or -delete items from the local cache, see {DatabaseStore::persistModel} -and {DatabaseStore::unpersistModel}. +delete items from the local cache, see {DatabaseStore::inTransaction} +and {DatabaseTransaction::persistModel}. **Simple Example:** Fetch a thread -```coffee -query = db.find(Thread, '123a2sc1ef4131') -query.then (thread) -> +```js +const query = db.find(Thread, '123a2sc1ef4131'); +query.then((thread) { // thread or null +}); ``` **Advanced Example:** Fetch 50 threads in the inbox, in descending order ```coffee -query = db.findAll(Thread) +const query = db.findAll(Thread); query.where([Thread.attributes.categories.contains('label-id')]) .order([Thread.attributes.lastMessageReceivedTimestamp.descending()]) .limit(100).offset(50) - .then (threads) -> + .then((threads) { // array of threads +}); ``` - -Section: Database */ export default class ModelQuery { @@ -58,6 +59,9 @@ export default class ModelQuery { this._count = false; } + /** + @returns {Query} - A deep copy of the Query that can be modified. + */ clone() { const q = new ModelQuery(this._klass, this._database).where(this._matchers).order(this._orders); q._orders = [].concat(this._orders);