UNPKG

@grindife/supamelon

Version:

Combination of supabase and watermelondb

409 lines (396 loc) 14.2 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); exports.__esModule = true; exports.default = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var _rx = require("../utils/rx"); var _invariant = _interopRequireDefault(require("../utils/common/invariant")); var _fp = require("../utils/fp"); var _Result = require("../utils/fp/Result"); var _Query = _interopRequireDefault(require("../Query")); var _RecordCache = _interopRequireDefault(require("./RecordCache")); var Collection = exports.default = /*#__PURE__*/function () { function Collection(database, ModelClass) { var _this = this; /** * `isLocalOnly` boolean to indicate if we should fetch a record from supabase if it is not found in local db */ /** * `Model` subclass associated with this Collection */ /** * An `Rx.Subject` that emits a signal on every change (record creation/update/deletion) in * this Collection. * * The emissions contain information about which record was changed and what the change was. * * Warning: You can easily introduce performance bugs in your application by using this method * inappropriately. You generally should just use the `Query` API. */ this.changes = new _rx.Subject(); this._subscribers = []; this.database = database; this.isLocalOnly = database.schema.tables[ModelClass.table].isLocalOnly || false; this.modelClass = ModelClass; this._cache = new _RecordCache.default(ModelClass.table, function (raw) { return new ModelClass(_this, raw); }, this); } /** * `Database` associated with this Collection. */ var _proto = Collection.prototype; /** * Fetches the record with the given ID. * * If the record is not found, the Promise will reject. */ _proto.find = function (id, supa) { return new Promise(function ($return) { var _this2 = this; return $return((0, _Result.toPromise)(function (callback) { return _this2._fetchRecord(id, callback, supa); })); }.bind(this)); } /** * Fetches the given record and then starts observing it. * * This is a convenience method that's equivalent to * `collection.find(id)`, followed by `record.observe()`. */; _proto.findAndObserve = function (id, supa) { var _this3 = this; return _rx.Observable.create(function (observer) { var unsubscribe = null; var unsubscribed = false; _this3._fetchRecord(id, function (result) { if (result.value) { var record = result.value; observer.next(record); unsubscribe = record.experimentalSubscribe(function (isDeleted) { if (!unsubscribed) { isDeleted ? observer.complete() : observer.next(record); } }); } else { // $FlowFixMe observer.error(result.error); } }, supa); return function () { unsubscribed = true; unsubscribe && unsubscribe(); }; }); }; /** * Returns a `Query` with conditions given. * * You can pass conditions as multiple arguments or a single array. * * See docs for details about the Query API. */ // $FlowFixMe _proto.query = function (...args) { var clauses = (0, _fp.fromArrayOrSpread)(args, 'Collection.query', 'Clause'); return new _Query.default(this, clauses); } /** * Creates a new record. * Pass a function to set attributes of the new record. * * Note: This method must be called within a Writer {@link Database#write}. * * @example * ```js * db.get(Tables.tasks).create(task => { * task.name = 'Task name' * }) * ``` */; _proto.create = function (recordBuilder = _fp.noop) { return new Promise(function ($return, $error) { var record; this.database._ensureInWriter("Collection.create()"); record = this.prepareCreate(recordBuilder); return Promise.resolve(this.database.batch(record)).then(function () { try { return $return(record); } catch ($boundEx) { return $error($boundEx); } }, $error); }.bind(this)); } /** * Prepares a new record to be created * * Use this to batch-execute multiple changes at once. * @see {Collection#create} * @see {Database#batch} */; _proto.prepareCreate = function (recordBuilder = _fp.noop) { // $FlowFixMe return this.modelClass._prepareCreate(this, recordBuilder); } /** * Prepares a new record to be created, based on a raw object. * * Don't use this unless you know how RawRecords work in WatermelonDB. See docs for more details. * * This is useful as a performance optimization, when adding online-only features to an otherwise * offline-first app, or if you're implementing your own sync mechanism. */; _proto.prepareCreateFromDirtyRaw = function (dirtyRaw) { // $FlowFixMe return this.modelClass._prepareCreateFromDirtyRaw(this, dirtyRaw); } /** * Returns a disposable record, based on a raw object. * * A disposable record is a read-only record that **does not** exist in the actual database. It's * not cached and cannot be saved in the database, updated, deleted, queried, or found by ID. It * only exists for as long as you keep a reference to it. * * Don't use this unless you know how RawRecords work in WatermelonDB. See docs for more details. * * This is useful for adding online-only features to an otherwise offline-first app, or for * temporary objects that are not meant to be persisted (as you can reuse existing Model helpers * and compatible UI components to display a disposable record). */; _proto.disposableFromDirtyRaw = function (dirtyRaw) { // $FlowFixMe return this.modelClass._disposableFromDirtyRaw(this, dirtyRaw); } // *** Implementation details *** // See: Query.fetch ; _proto._fetchQuery = function (query, callback) { var _this4 = this; this.database.adapter.underlyingAdapter.query(query.serialize(), function (result) { return callback((0, _Result.mapValue)(function (rawRecords) { return _this4._cache.recordsFromQueryResult(rawRecords); }, result)); }); }; _proto._fetchIds = function (query, callback) { this.database.adapter.underlyingAdapter.queryIds(query.serialize(), callback); }; _proto._fetchCount = function (query, callback) { this.database.adapter.underlyingAdapter.count(query.serialize(), callback); }; _proto._unsafeFetchRaw = function (query, callback) { this.database.adapter.underlyingAdapter.unsafeQueryRaw(query.serialize(), callback); } // Fetches the record from supabase if not found in local db ; _proto._fetchFromSupabaseAndPersist = function (id, callback) { return new Promise(function ($return, $error) { var _this5, data, error, cachedRecord; _this5 = this; var $Try_1_Post = function $Try_1_Post() { try { return $return(); } catch ($boundEx) { return $error($boundEx); } }; var $Try_1_Catch = function $Try_1_Catch(supabaseError) { try { console.error('Error fetching from Supabase', supabaseError); // Keep the console error for debugging callback({ error: new Error("Failed to fetch record from Supabase: ".concat(supabaseError.message)) }); return $Try_1_Post(); } catch ($boundEx) { return $error($boundEx); } }; try { return Promise.resolve(this.database.supabase.from(this.table).select('*').eq('id', id).single()).then(function ($await_5) { try { ({ data: data, error: error } = $await_5); if (error || !data) { callback({ error: new Error("Record ".concat(this.table, "#").concat(id, " not found in DB or Supabase: ").concat((null === error || void 0 === error ? void 0 : error.message) || 'No data returned')) }); return $return(); } var $Try_2_Post = function $Try_2_Post() { try { return $Try_1_Post(); } catch ($boundEx) { return $Try_1_Catch($boundEx); } }; var $Try_2_Catch = function $Try_2_Catch(localDbError) { try { console.error('Error inserting into local db', localDbError); // Keep the console error for debugging callback({ error: new Error("Failed to insert record into local DB: ".concat(localDbError.message)) }); return $Try_2_Post(); } catch ($boundEx) { return $Try_1_Catch($boundEx); } }; try { return Promise.resolve(this.database.write(function () { return new Promise(function ($return, $error) { var record = _this5.prepareCreateFromDirtyRaw((0, _extends2.default)({}, data, { _status: 'synced', _changed: '' })); return Promise.resolve(_this5.database.batch(record)).then(function () { try { return $return(); } catch ($boundEx) { return $error($boundEx); } }, $error); }); })).then(function () { try { cachedRecord = this._cache.get(id); if (cachedRecord) { return $return(callback({ value: cachedRecord })); } else { // This should almost never happen, but handle it just in case callback({ error: new Error("Failed to retrieve record from cache after insert ".concat(this.table, "#").concat(id)) }); } return $Try_2_Post(); } catch ($boundEx) { return $Try_2_Catch($boundEx); } }.bind(this), $Try_2_Catch); } catch (localDbError) { $Try_2_Catch(localDbError) } } catch ($boundEx) { return $Try_1_Catch($boundEx); } }.bind(this), $Try_1_Catch); } catch (supabaseError) { $Try_1_Catch(supabaseError) } }.bind(this)); } // Fetches exactly one record (See: Collection.find) ; _proto._fetchRecord = function (id, callback, supa = true) { var _this6 = this; if ('string' !== typeof id) { callback({ error: new Error("Invalid record ID ".concat(this.table, "#").concat(id)) }); return; } var cachedRecord = this._cache.get(id); if (cachedRecord) { callback({ value: cachedRecord }); return; } this.database.adapter.underlyingAdapter.find(this.table, id, function (result) { return new Promise(function ($return, $error) { if (supa && result.error || supa && !result.value && !_this6.isLocalOnly) { return Promise.resolve(_this6._fetchFromSupabaseAndPersist(id, callback)).then($return, $error); } else { callback((0, _Result.mapValue)(function (rawRecord) { if (!rawRecord) { return null; // Return null if the record does not exist } return _this6._cache.recordFromQueryResult(rawRecord); }, result)); return function () { return $return(); }.call(this); } return function () { return $return(); }.call(this); }); }); }; _proto._applyChangesToCache = function (operations) { var _this7 = this; operations.forEach(function ({ record: record, type: type }) { if ('created' === type) { record._preparedState = null; _this7._cache.add(record); } else if ('destroyed' === type) { _this7._cache.delete(record); } }); }; _proto._notify = function (operations) { this._subscribers.forEach(function ([subscriber]) { subscriber(operations); }); this.changes.next(operations); operations.forEach(function ({ record: record, type: type }) { if ('updated' === type) { record._notifyChanged(); } else if ('destroyed' === type) { record._notifyDestroyed(); } }); }; /** * Notifies `subscriber` on every change (record creation/update/deletion) in this Collection. * * Notifications contain information about which record was changed and what the change was. * (Currently, subscribers are called before `changes` emissions, but this behavior might change) * * Warning: You can easily introduce performance bugs in your application by using this method * inappropriately. You generally should just use the `Query` API. */ _proto.experimentalSubscribe = function (subscriber, debugInfo) { var _this8 = this; var entry = [subscriber, debugInfo]; this._subscribers.push(entry); return function () { var idx = _this8._subscribers.indexOf(entry); -1 !== idx && _this8._subscribers.splice(idx, 1); }; }; return (0, _createClass2.default)(Collection, [{ key: "db", get: function get() { return this.database; } /** * Table name associated with this Collection */ }, { key: "table", get: function get() { // $FlowFixMe return this.modelClass.table; } /** * Table schema associated with this Collection */ }, { key: "schema", get: function get() { return this.database.schema.tables[this.table]; } }]); }();