From 7b4187f995a2683278cf314a182e835939ee7368 Mon Sep 17 00:00:00 2001 From: Jonathan Poltak Samosir Date: Tue, 4 Jun 2019 18:45:53 +0700 Subject: [PATCH 1/2] Add prettier + pre-commit hook + configs - mostly the same as in memex project - some memex-specific things removed from ignore file --- .prettierignore | 4 ++++ package.json | 11 ++++++++++- prettier.config.js | 6 ++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .prettierignore create mode 100644 prettier.config.js diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ac92f3f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +lib/ +node_modules/ +.git/ +package.json diff --git a/package.json b/package.json index cda2fb1..25a4964 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,13 @@ "prepare": "tsc", "prepare:watch": "npm run prepare -- -w", "test": "mocha --require ts-node/register \"ts/**/*.test.ts\"", - "test:watch": "mocha -r source-map-support/register -r ts-node/register \"ts/**/*.test.ts\" --watch --watch-extensions ts" + "test:watch": "mocha -r source-map-support/register -r ts-node/register \"ts/**/*.test.ts\" --watch --watch-extensions ts", + "format": "prettier --config prettier.config.js --write '**/*.{ts,js,tsx,jsx,css,md}'" + }, + "husky": { + "hooks": { + "pre-commit": "pretty-quick --staged" + } }, "keywords": [ "storage", @@ -35,7 +41,10 @@ "@worldbrain/storex": "^0.3.5", "expect": "^23.5.0", "fake-indexeddb": "^2.0.4", + "husky": "2.3.0", "mocha": "^4.0.1", + "prettier": "1.17.1", + "pretty-quick": "1.11.0", "ts-node": "^7.0.1", "typescript": "^3.0.1" }, diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..369f73e --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,6 @@ +module.exports = { + semi: false, + singleQuote: true, + trailingComma: 'all', + tabWidth: 4, +} From b2f9b0977cb6bd9bd04b3c856585281b120e5643 Mon Sep 17 00:00:00 2001 From: Jonathan Poltak Samosir Date: Tue, 4 Jun 2019 18:47:13 +0700 Subject: [PATCH 2/2] Run prettier to format codebase - should be a one-time thing --- ts/in-memory.ts | 4 +- ts/index.test.ts | 154 ++++++++++++++-------- ts/index.ts | 290 ++++++++++++++++++++++++++++++------------ ts/object-cleaning.ts | 113 +++++++++++----- ts/schema.test.ts | 6 +- ts/schema.ts | 61 +++++++-- ts/types.ts | 36 ++++-- ts/update-ops.ts | 9 +- ts/utils.ts | 14 +- 9 files changed, 478 insertions(+), 209 deletions(-) diff --git a/ts/in-memory.ts b/ts/in-memory.ts index 2224a5f..9540167 100644 --- a/ts/in-memory.ts +++ b/ts/in-memory.ts @@ -1,4 +1,4 @@ export default () => ({ - factory: new (require('fake-indexeddb/lib/FDBFactory')), - range: require('fake-indexeddb/lib/FDBKeyRange'), + factory: new (require('fake-indexeddb/lib/FDBFactory'))(), + range: require('fake-indexeddb/lib/FDBKeyRange'), }) diff --git a/ts/index.test.ts b/ts/index.test.ts index 2d164f3..0d43f50 100644 --- a/ts/index.test.ts +++ b/ts/index.test.ts @@ -1,51 +1,66 @@ import * as expect from 'expect' -import { testStorageBackend, testStorageBackendFullTextSearch } from "@worldbrain/storex/lib/index.tests" -import extractTerms from "@worldbrain/memex-stemmer"; -import { DexieStorageBackend } from "." +import { + testStorageBackend, + testStorageBackendFullTextSearch, +} from '@worldbrain/storex/lib/index.tests' +import extractTerms from '@worldbrain/memex-stemmer' +import { DexieStorageBackend } from '.' import inMemory from './in-memory' -import StorageManager from "@worldbrain/storex"; -import { _flattenBatch } from './utils'; +import StorageManager from '@worldbrain/storex' +import { _flattenBatch } from './utils' describe('Dexie StorageBackend integration tests', () => { testStorageBackend(async () => { - return new DexieStorageBackend({dbName: 'unittest', idbImplementation: inMemory()}) + return new DexieStorageBackend({ + dbName: 'unittest', + idbImplementation: inMemory(), + }) }) }) describe('Dexie StorageBackend full-text search with Memex stemmer tests', () => { describe('with single stemmer', () => { testStorageBackendFullTextSearch(async () => { - return new DexieStorageBackend({dbName: 'unittest', idbImplementation: inMemory(), stemmer: extractTerms}) + return new DexieStorageBackend({ + dbName: 'unittest', + idbImplementation: inMemory(), + stemmer: extractTerms, + }) }) }) describe('with stemmer selector', () => { testStorageBackendFullTextSearch(async () => { - return new DexieStorageBackend({dbName: 'unittest', idbImplementation: inMemory(), stemmerSelector: () => extractTerms}) + return new DexieStorageBackend({ + dbName: 'unittest', + idbImplementation: inMemory(), + stemmerSelector: () => extractTerms, + }) }) }) }) describe('Dexie StorageBackend batch operations', () => { - async function setupTest({userFields = null} = {}) { - const backend = new DexieStorageBackend({dbName: 'unittest', idbImplementation: inMemory()}) - const storageManager = new StorageManager({backend}) + async function setupTest({ userFields = null } = {}) { + const backend = new DexieStorageBackend({ + dbName: 'unittest', + idbImplementation: inMemory(), + }) + const storageManager = new StorageManager({ backend }) storageManager.registry.registerCollections({ user: { version: new Date(2019, 1, 1), fields: userFields || { - displayName: {type: 'string'} - } + displayName: { type: 'string' }, + }, }, email: { version: new Date(2019, 1, 1), fields: { - displayName: {type: 'string'} + displayName: { type: 'string' }, }, - relationships: [ - {childOf: 'user'} - ] - } + relationships: [{ childOf: 'user' }], + }, }) await storageManager.finishInitialization() return { storageManager } @@ -53,45 +68,70 @@ describe('Dexie StorageBackend batch operations', () => { it('should support batches with updateObject operations', async () => { const { storageManager } = await setupTest() - const { object: object1 } = await storageManager.collection('user').createObject({displayName: 'Jack'}) - const { object: object2 } = await storageManager.collection('user').createObject({displayName: 'Jane'}) + const { object: object1 } = await storageManager + .collection('user') + .createObject({ displayName: 'Jack' }) + const { object: object2 } = await storageManager + .collection('user') + .createObject({ displayName: 'Jane' }) await storageManager.operation('executeBatch', [ - { operation: 'updateObjects', collection: 'user', where: {id: object1.id}, updates: {displayName: 'Jack 2'} }, - { operation: 'updateObjects', collection: 'user', where: {id: object2.id}, updates: {displayName: 'Jane 2'} }, + { + operation: 'updateObjects', + collection: 'user', + where: { id: object1.id }, + updates: { displayName: 'Jack 2' }, + }, + { + operation: 'updateObjects', + collection: 'user', + where: { id: object2.id }, + updates: { displayName: 'Jane 2' }, + }, ]) expect([ - await storageManager.collection('user').findOneObject({id: object1.id}), - await storageManager.collection('user').findOneObject({id: object2.id}), + await storageManager + .collection('user') + .findOneObject({ id: object1.id }), + await storageManager + .collection('user') + .findOneObject({ id: object2.id }), ]).toEqual([ - {id: object1.id, displayName: 'Jack 2'}, - {id: object2.id, displayName: 'Jane 2'}, + { id: object1.id, displayName: 'Jack 2' }, + { id: object2.id, displayName: 'Jane 2' }, ]) }) describe('flattenBatch()', () => { it('should flatten batches with complex creates', async () => { const { storageManager } = await setupTest() - expect(_flattenBatch([ - { - placeholder: 'jane', - operation: 'createObject', - collection: 'user', - args: { - displayName: 'Jane', - emails: [{ - address: 'jane@doe.com' - }] - } - }, - { - placeholder: 'joe', - operation: 'createObject', - collection: 'user', - args: { - displayName: 'Joe' - } - }, - ], storageManager.registry)).toEqual([ + expect( + _flattenBatch( + [ + { + placeholder: 'jane', + operation: 'createObject', + collection: 'user', + args: { + displayName: 'Jane', + emails: [ + { + address: 'jane@doe.com', + }, + ], + }, + }, + { + placeholder: 'joe', + operation: 'createObject', + collection: 'user', + args: { + displayName: 'Joe', + }, + }, + ], + storageManager.registry, + ), + ).toEqual([ { placeholder: 'jane', operation: 'createObject', @@ -99,30 +139,32 @@ describe('Dexie StorageBackend batch operations', () => { args: { displayName: 'Jane', }, - replace: [] + replace: [], }, { placeholder: 'auto-gen:1', operation: 'createObject', collection: 'email', args: { - address: 'jane@doe.com' + address: 'jane@doe.com', }, - replace: [{ - path: 'user', - placeholder: 'jane', - }] + replace: [ + { + path: 'user', + placeholder: 'jane', + }, + ], }, { placeholder: 'joe', operation: 'createObject', collection: 'user', args: { - displayName: 'Joe' + displayName: 'Joe', }, - replace: [] + replace: [], }, ]) }) - }) + }) }) diff --git a/ts/index.ts b/ts/index.ts index 66404d5..294fd4e 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -2,17 +2,31 @@ import Dexie from 'dexie' import 'dexie-mongoify' import { StorageRegistry, CollectionDefinition } from '@worldbrain/storex' -import { CreateObjectDissection, dissectCreateObjectOperation, convertCreateObjectDissectionToBatch, setIn } from '@worldbrain/storex/lib/utils' +import { + CreateObjectDissection, + dissectCreateObjectOperation, + convertCreateObjectDissectionToBatch, + setIn, +} from '@worldbrain/storex/lib/utils' // import { CollectionDefinition } from 'storex/types' import * as backend from '@worldbrain/storex/lib/types/backend' import { getDexieHistory } from './schema' import { DexieMongoify, DexieSchema } from './types' -import { StorageBackendFeatureSupport } from '@worldbrain/storex/lib/types/backend-features'; -import { UnimplementedError, InvalidOptionsError } from '@worldbrain/storex/lib/types/errors'; -import { _flattenBatch } from './utils'; +import { StorageBackendFeatureSupport } from '@worldbrain/storex/lib/types/backend-features' +import { + UnimplementedError, + InvalidOptionsError, +} from '@worldbrain/storex/lib/types/errors' +import { _flattenBatch } from './utils' import { StemmerSelector, Stemmer, SchemaPatcher } from './types' -import { _processFieldUpdates } from './update-ops'; -import { makeCleanerChain, _makeCustomFieldCleaner, _cleanFullTextIndexFieldsForWrite, _cleanFieldAliasesForWrites, _cleanFieldAliasesForReads } from './object-cleaning'; +import { _processFieldUpdates } from './update-ops' +import { + makeCleanerChain, + _makeCustomFieldCleaner, + _cleanFullTextIndexFieldsForWrite, + _cleanFieldAliasesForWrites, + _cleanFieldAliasesForReads, +} from './object-cleaning' export { Stemmer, StemmerSelector, SchemaPatcher } from './types' export interface IndexedDbImplementation { @@ -44,11 +58,11 @@ export class DexieStorageBackend extends backend.StorageBackend { transaction: true, } - private dbName : string - private idbImplementation : IndexedDbImplementation - private dexie! : DexieMongoify - private stemmerSelector : StemmerSelector - private schemaPatcher : SchemaPatcher + private dbName: string + private idbImplementation: IndexedDbImplementation + private dexie!: DexieMongoify + private stemmerSelector: StemmerSelector + private schemaPatcher: SchemaPatcher private initialized = false private readObjectCleaner = makeCleanerChain([ _cleanFieldAliasesForReads, @@ -85,11 +99,16 @@ export class DexieStorageBackend extends backend.StorageBackend { stemmerSelector = () => null } } else if (stemmer) { - throw new Error(`You cannot pass both a 'stemmer' and a 'stemmerSelector' into DexieStorageBackend`) + throw new Error( + `You cannot pass both a 'stemmer' and a 'stemmerSelector' into DexieStorageBackend`, + ) } this.dbName = dbName - this.idbImplementation = idbImplementation || { factory: window.indexedDB, range: window['IDBKeyRange'] } + this.idbImplementation = idbImplementation || { + factory: window.indexedDB, + range: window['IDBKeyRange'], + } this.stemmerSelector = stemmerSelector this.schemaPatcher = schemaPatcher } @@ -126,14 +145,16 @@ export class DexieStorageBackend extends backend.StorageBackend { } // See if we're trying to create full-text indices without providing a stemmer - for (const [collectionName, collectionDefinition] of Object.entries(this.registry.collections)) { + for (const [collectionName, collectionDefinition] of Object.entries( + this.registry.collections, + )) { for (const index of collectionDefinition.indices || []) { if (typeof index === 'string') { const field = collectionDefinition.fields[index] if (field.type === 'text') { throw new Error( `Trying to create full-text index on '${collectionName}.${index}' - without having supplied a stemmer to the Dexie back-end` + without having supplied a stemmer to the Dexie back-end`, ) } } @@ -144,12 +165,14 @@ export class DexieStorageBackend extends backend.StorageBackend { _initDexie = () => { this.dexie = new Dexie(this.dbName, { indexedDB: this.idbImplementation.factory, - IDBKeyRange: this.idbImplementation.range + IDBKeyRange: this.idbImplementation.range, }) as DexieMongoify // DexieMongofiy binds the .collection to the last DB created, creating confusing situations when using multiple DBs at the same time - Dexie.prototype['collection'] = function collection(collectionName : string) { - return this.table(collectionName); + Dexie.prototype['collection'] = function collection( + collectionName: string, + ) { + return this.table(collectionName) } const dexieHistory = getDexieHistory(this.registry) @@ -161,40 +184,77 @@ export class DexieStorageBackend extends backend.StorageBackend { async migrate({ database }: { database?: string } = {}) { if (database) { - throw new Error('This backend doesn\'t support multiple databases directly') + throw new Error( + "This backend doesn't support multiple databases directly", + ) } } - async cleanup(): Promise { - - } + async cleanup(): Promise {} - async createObject(collection: string, object : any, options: backend.CreateSingleOptions = {}): Promise { - return this._complexCreateObject(collection, object, {...options, needsRawCreates: false}) + async createObject( + collection: string, + object: any, + options: backend.CreateSingleOptions = {}, + ): Promise { + return this._complexCreateObject(collection, object, { + ...options, + needsRawCreates: false, + }) } - async _complexCreateObject(collection: string, object : any, options: backend.CreateSingleOptions & {needsRawCreates : boolean}) { - const dissection = dissectCreateObjectOperation({operation: 'createObject', collection, args: object}, this.registry) + async _complexCreateObject( + collection: string, + object: any, + options: backend.CreateSingleOptions & { needsRawCreates: boolean }, + ) { + const dissection = dissectCreateObjectOperation( + { operation: 'createObject', collection, args: object }, + this.registry, + ) const batchToExecute = convertCreateObjectDissectionToBatch(dissection) - const batchResult = await this._rawExecuteBatch(batchToExecute, {needsRawCreates: true}) - this._reconstructCreatedObject(object, collection, dissection, batchResult.info) + const batchResult = await this._rawExecuteBatch(batchToExecute, { + needsRawCreates: true, + }) + this._reconstructCreatedObject( + object, + collection, + dissection, + batchResult.info, + ) return { object } } - async _reconstructCreatedObject(object : any, collection : string, operationDissection : CreateObjectDissection, batchResultInfo : any) { + async _reconstructCreatedObject( + object: any, + collection: string, + operationDissection: CreateObjectDissection, + batchResultInfo: any, + ) { for (const step of operationDissection.objects) { const collectionDefiniton = this.registry.collections[collection] const pkIndex = collectionDefiniton.pkIndex - setIn(object, [...step.path, pkIndex], batchResultInfo[step.placeholder].object[pkIndex as string]) + setIn( + object, + [...step.path, pkIndex], + batchResultInfo[step.placeholder].object[pkIndex as string], + ) } } - async _rawCreateObject(collection: string, object : any, options: backend.CreateSingleOptions = {}) { - const { collectionDefinition } = this._prepareOperation({ operationName: 'createObject', collection }) - + async _rawCreateObject( + collection: string, + object: any, + options: backend.CreateSingleOptions = {}, + ) { + const { collectionDefinition } = this._prepareOperation({ + operationName: 'createObject', + collection, + }) + await this.createObjectCleaner(object, { - collectionDefinition, + collectionDefinition, stemmerSelector: this.stemmerSelector, }) await this.dexie.table(collection).put(object) @@ -203,7 +263,11 @@ export class DexieStorageBackend extends backend.StorageBackend { } // TODO: Afford full find support for ignoreCase opt; currently just uses the first filter entry - private _findIgnoreCase(collection: string, query : any, findOpts: backend.FindManyOptions = {}) { + private _findIgnoreCase( + collection: string, + query: any, + findOpts: backend.FindManyOptions = {}, + ) { // Grab first entry from the filter query; ignore rest for now const [[indexName, value], ...fields] = Object.entries(query) @@ -215,7 +279,9 @@ export class DexieStorageBackend extends backend.StorageBackend { if (findOpts.ignoreCase && findOpts.ignoreCase[0] !== indexName) { throw new InvalidOptionsError( - `Specified ignoreCase field '${findOpts.ignoreCase[0]}' is not in filter query.`, + `Specified ignoreCase field '${ + findOpts.ignoreCase[0] + }' is not in filter query.`, ) } @@ -225,20 +291,32 @@ export class DexieStorageBackend extends backend.StorageBackend { .equalsIgnoreCase(value) } + async findObjects( + collection: string, + query: any, + findOpts: backend.FindManyOptions = {}, + ): Promise> { + const { collectionDefinition } = this._prepareOperation({ + operationName: 'findObjects', + collection, + }) - async findObjects(collection : string, query : any, findOpts: backend.FindManyOptions = {}): Promise> { - const { collectionDefinition } = this._prepareOperation({ operationName: 'findObjects', collection }) - - const order = findOpts.order && findOpts.order.length ? findOpts.order[0] : null + const order = + findOpts.order && findOpts.order.length ? findOpts.order[0] : null if (order && findOpts.order!.length > 1) { throw new Error('Sorting on multiple fields is not supported') } - const descendingOrder = findOpts.reverse || (order && order[1] == 'desc') + const descendingOrder = + findOpts.reverse || (order && order[1] == 'desc') - await this.whereObjectCleaner(query, { collectionDefinition, stemmerSelector: this.stemmerSelector }) - let coll = findOpts.ignoreCase && findOpts.ignoreCase.length - ? this._findIgnoreCase(collection, query, findOpts) - : this.dexie.collection(collection).find(query) + await this.whereObjectCleaner(query, { + collectionDefinition, + stemmerSelector: this.stemmerSelector, + }) + let coll = + findOpts.ignoreCase && findOpts.ignoreCase.length + ? this._findIgnoreCase(collection, query, findOpts) + : this.dexie.collection(collection).find(query) let results if (order) { @@ -260,20 +338,30 @@ export class DexieStorageBackend extends backend.StorageBackend { results = await coll.toArray() } - - await Promise.all(results.map(async object => { - await this.readObjectCleaner(object, { - collectionDefinition, - stemmerSelector: this.stemmerSelector, - }) - })) - + + await Promise.all( + results.map(async object => { + await this.readObjectCleaner(object, { + collectionDefinition, + stemmerSelector: this.stemmerSelector, + }) + }), + ) + return results } - async updateObjects(collection: string, where : any, updates : any, options: backend.UpdateManyOptions = {}): Promise { - const { collectionDefinition } = this._prepareOperation({ operationName: 'updateObjects', collection }) - + async updateObjects( + collection: string, + where: any, + updates: any, + options: backend.UpdateManyOptions = {}, + ): Promise { + const { collectionDefinition } = this._prepareOperation({ + operationName: 'updateObjects', + collection, + }) + await this.updateObjectCleaner(updates, { collectionDefinition, stemmerSelector: this.stemmerSelector, @@ -287,9 +375,16 @@ export class DexieStorageBackend extends backend.StorageBackend { } } - async deleteObjects(collection : string, query : any, options : backend.DeleteManyOptions = {}): Promise { - const { collectionDefinition } = this._prepareOperation({ operationName: 'deleteObjects', collection }) - + async deleteObjects( + collection: string, + query: any, + options: backend.DeleteManyOptions = {}, + ): Promise { + const { collectionDefinition } = this._prepareOperation({ + operationName: 'deleteObjects', + collection, + }) + this.whereObjectCleaner(query, { collectionDefinition, stemmerSelector: this.stemmerSelector, @@ -302,32 +397,44 @@ export class DexieStorageBackend extends backend.StorageBackend { // return deletedCount } - async countObjects(collection: string, query : any) { + async countObjects(collection: string, query: any) { return this.dexie.collection(collection).count(query) } - async executeBatch(batch : backend.OperationBatch) { + async executeBatch(batch: backend.OperationBatch) { if (!batch.length) { return { info: {} } } - const collections = Array.from(new Set(_flattenBatch(batch, this.registry).map(operation => operation.collection))) + const collections = Array.from( + new Set( + _flattenBatch(batch, this.registry).map( + operation => operation.collection, + ), + ), + ) let info = null await this.transaction({ collections }, async () => { - info = (await this._rawExecuteBatch(batch, {needsRawCreates: false})).info + info = (await this._rawExecuteBatch(batch, { + needsRawCreates: false, + })).info }) return { info } } - async transaction(options : { collections: string[] }, body : Function) { + async transaction(options: { collections: string[] }, body: Function) { const executeBody = async () => { - return body({ transactionOperation: (name : string, ...args : any[]) => { - return this.operation(name, ...args) - } }) + return body({ + transactionOperation: (name: string, ...args: any[]) => { + return this.operation(name, ...args) + }, + }) } if (typeof navigator !== 'undefined') { - const tables = options.collections.map(collection => this.dexie.table(collection)) + const tables = options.collections.map(collection => + this.dexie.table(collection), + ) return this.dexie.transaction('rw', tables, executeBody) } else { return executeBody() @@ -335,44 +442,67 @@ export class DexieStorageBackend extends backend.StorageBackend { } async _rawExecuteBatch( - batch : backend.OperationBatch, - options : {needsRawCreates : boolean} + batch: backend.OperationBatch, + options: { needsRawCreates: boolean }, ) { const info = {} const placeholders = {} for (const operation of batch) { if (operation.operation === 'createObject') { for (const { path, placeholder } of operation.replace || []) { - operation.args[path as string] = placeholders[placeholder].id + operation.args[path as string] = + placeholders[placeholder].id } const { object } = options.needsRawCreates - ? await this._rawCreateObject(operation.collection, operation.args) - : await this._complexCreateObject(operation.collection, operation.args, {needsRawCreates: true}) + ? await this._rawCreateObject( + operation.collection, + operation.args, + ) + : await this._complexCreateObject( + operation.collection, + operation.args, + { needsRawCreates: true }, + ) if (operation.placeholder) { - info[operation.placeholder] = {object} + info[operation.placeholder] = { object } placeholders[operation.placeholder] = object } } else if (operation.operation === 'updateObjects') { - await this.updateObjects(operation.collection, operation.where, operation.updates) + await this.updateObjects( + operation.collection, + operation.where, + operation.updates, + ) } } return { info } } - async operation(name : string, ...args : any[]) { + async operation(name: string, ...args: any[]) { if (!this.initialized) { - throw new Error('Tried to use Dexie backend without calling StorageManager.finishInitialization() first') + throw new Error( + 'Tried to use Dexie backend without calling StorageManager.finishInitialization() first', + ) } // console.log('operation', name) return await super.operation(name, ...args) } - _prepareOperation(options : { operationName : string, collection : string }) : { collectionDefinition : CollectionDefinition } { - const collectionDefinition = this.registry.collections[options.collection] + _prepareOperation(options: { + operationName: string + collection: string + }): { collectionDefinition: CollectionDefinition } { + const collectionDefinition = this.registry.collections[ + options.collection + ] if (!collectionDefinition) { - throw new Error(`Tried to do '${options.operationName}' operation on non-existing collection: ${options.collection}`) + throw new Error( + `Tried to do '${ + options.operationName + }' operation on non-existing collection: ${options.collection}`, + ) } return { collectionDefinition } } diff --git a/ts/object-cleaning.ts b/ts/object-cleaning.ts index a2a989d..f8d0620 100644 --- a/ts/object-cleaning.ts +++ b/ts/object-cleaning.ts @@ -1,9 +1,12 @@ -import { getTermsIndex } from "./schema"; -import { ObjectCleaner, ObjectCleanerOptions } from "./types"; -import { isChildOfRelationship, isConnectsRelationship } from "@worldbrain/storex"; +import { getTermsIndex } from './schema' +import { ObjectCleaner, ObjectCleanerOptions } from './types' +import { + isChildOfRelationship, + isConnectsRelationship, +} from '@worldbrain/storex' -export function makeCleanerChain(cleaners : ObjectCleaner[]) : ObjectCleaner { - return async (object : any, options : ObjectCleanerOptions) => { +export function makeCleanerChain(cleaners: ObjectCleaner[]): ObjectCleaner { + return async (object: any, options: ObjectCleanerOptions) => { for (const cleaner of cleaners) { object = (await cleaner(object, options)) || object } @@ -15,8 +18,13 @@ export function makeCleanerChain(cleaners : ObjectCleaner[]) : ObjectCleaner { * Handles mutation of a document to be inserted/updated to storage, * depending on needed pre-processing for a given indexed field. */ -export const _cleanFullTextIndexFieldsForWrite : ObjectCleaner = async (object : any, options : ObjectCleanerOptions) => { - for (const [fieldName, fieldDef] of Object.entries(options.collectionDefinition.fields)) { +export const _cleanFullTextIndexFieldsForWrite: ObjectCleaner = async ( + object: any, + options: ObjectCleanerOptions, +) => { + for (const [fieldName, fieldDef] of Object.entries( + options.collectionDefinition.fields, + )) { switch (fieldDef.type) { case 'text': if (fieldDef._index == null) { @@ -27,20 +35,28 @@ export const _cleanFullTextIndexFieldsForWrite : ObjectCleaner = async (object : if (!fieldValue) { continue } - + if (!options.stemmerSelector) { - throw new Error(`You tried to write to an indexed text field (${fieldName}) without specifying a stemmer selector`) + throw new Error( + `You tried to write to an indexed text field (${fieldName}) without specifying a stemmer selector`, + ) } - const stemmer = options.stemmerSelector({ collectionName: options.collectionDefinition.name!, fieldName }) + const stemmer = options.stemmerSelector({ + collectionName: options.collectionDefinition.name!, + fieldName, + }) if (!stemmer) { - throw new Error(`You tried to write to an indexed text field (${fieldName}) without specifying a stemmer for that field`) + throw new Error( + `You tried to write to an indexed text field (${fieldName}) without specifying a stemmer for that field`, + ) } - const indexDef = options.collectionDefinition.indices![fieldDef._index] + const indexDef = options.collectionDefinition.indices![ + fieldDef._index + ] const fullTextField = - indexDef.fullTextIndexName || - getTermsIndex(fieldName) + indexDef.fullTextIndexName || getTermsIndex(fieldName) object[fullTextField] = [...stemmer(fieldValue)] break default: @@ -48,58 +64,85 @@ export const _cleanFullTextIndexFieldsForWrite : ObjectCleaner = async (object : } } -export function _makeCustomFieldCleaner(options : { purpose : 'query-where' | 'create' | 'update' | 'read' }) : ObjectCleaner { +export function _makeCustomFieldCleaner(options: { + purpose: 'query-where' | 'create' | 'update' | 'read' +}): ObjectCleaner { const purpose = options.purpose const direction = purpose === 'read' ? 'from-storage' : 'to-storage' - const method = direction === 'from-storage' ? 'prepareFromStorage' : 'prepareForStorage' - return async (object : any, options : ObjectCleanerOptions) => { - for (const [fieldName, fieldDef] of Object.entries(options.collectionDefinition.fields)) { + const method = + direction === 'from-storage' + ? 'prepareFromStorage' + : 'prepareForStorage' + return async (object: any, options: ObjectCleanerOptions) => { + for (const [fieldName, fieldDef] of Object.entries( + options.collectionDefinition.fields, + )) { if (fieldDef.fieldObject) { const oldValue = object[fieldName] - if (purpose !== 'create' && !Object.keys(object).includes(fieldName)) { + if ( + purpose !== 'create' && + !Object.keys(object).includes(fieldName) + ) { continue } - const newValue = await fieldDef.fieldObject[method]( - oldValue, - ) + const newValue = await fieldDef.fieldObject[method](oldValue) object[fieldName] = newValue } } } } -export const _cleanFieldAliasesForWrites : ObjectCleaner = async (object : any, options : ObjectCleanerOptions) => { - for (const relationship of options.collectionDefinition.relationships || []) { +export const _cleanFieldAliasesForWrites: ObjectCleaner = async ( + object: any, + options: ObjectCleanerOptions, +) => { + for (const relationship of options.collectionDefinition.relationships || + []) { if (isChildOfRelationship(relationship)) { - if (relationship.alias !== relationship.fieldName && typeof object[relationship.alias!] !== 'undefined') { + if ( + relationship.alias !== relationship.fieldName && + typeof object[relationship.alias!] !== 'undefined' + ) { object[relationship.fieldName!] = object[relationship.alias!] delete object[relationship.alias!] } } else if (isConnectsRelationship(relationship)) { - if (relationship.aliases![0] !== relationship.fieldNames![0] && typeof object[relationship.aliases![0]] !== 'undefined') { - + if ( + relationship.aliases![0] !== relationship.fieldNames![0] && + typeof object[relationship.aliases![0]] !== 'undefined' + ) { } - if (relationship.aliases![1] !== relationship.fieldNames![1] && typeof object[relationship.aliases![1]] !== 'undefined') { - + if ( + relationship.aliases![1] !== relationship.fieldNames![1] && + typeof object[relationship.aliases![1]] !== 'undefined' + ) { } } } } -export const _cleanFieldAliasesForReads : ObjectCleaner = async (object : any, options : ObjectCleanerOptions) => { - for (const relationship of options.collectionDefinition.relationships || []) { +export const _cleanFieldAliasesForReads: ObjectCleaner = async ( + object: any, + options: ObjectCleanerOptions, +) => { + for (const relationship of options.collectionDefinition.relationships || + []) { if (isChildOfRelationship(relationship)) { if (relationship.alias !== relationship.fieldName) { object[relationship.alias!] = object[relationship.fieldName!] delete object[relationship.fieldName!] } } else if (isConnectsRelationship(relationship)) { - if (relationship.aliases![0] !== relationship.fieldNames![0] && typeof object[relationship.aliases![0]] !== 'undefined') { - + if ( + relationship.aliases![0] !== relationship.fieldNames![0] && + typeof object[relationship.aliases![0]] !== 'undefined' + ) { } - if (relationship.aliases![1] !== relationship.fieldNames![1] && typeof object[relationship.aliases![1]] !== 'undefined') { - + if ( + relationship.aliases![1] !== relationship.fieldNames![1] && + typeof object[relationship.aliases![1]] !== 'undefined' + ) { } } } diff --git a/ts/schema.test.ts b/ts/schema.test.ts index 4626043..f347767 100644 --- a/ts/schema.test.ts +++ b/ts/schema.test.ts @@ -2,11 +2,13 @@ const expect = require('expect') import StorageRegisty from '@worldbrain/storex/lib/registry' import { getDexieHistory } from './schema' -import { FieldTypeRegistry } from '@worldbrain/storex/lib/fields'; +import { FieldTypeRegistry } from '@worldbrain/storex/lib/fields' describe('Dexie schema generation', () => { it('it should work', () => { - const storageRegisty = new StorageRegisty({ fieldTypes: new FieldTypeRegistry() }) + const storageRegisty = new StorageRegisty({ + fieldTypes: new FieldTypeRegistry(), + }) storageRegisty.registerCollection('spam', { version: new Date(2018, 5, 20), fields: { diff --git a/ts/schema.ts b/ts/schema.ts index 55feea1..3ffa423 100644 --- a/ts/schema.ts +++ b/ts/schema.ts @@ -1,6 +1,14 @@ -import StorageRegistry, { RegistryCollections } from '@worldbrain/storex/lib/registry' +import StorageRegistry, { + RegistryCollections, +} from '@worldbrain/storex/lib/registry' import { DexieSchema } from './types' -import { CollectionDefinition, isRelationshipReference, isChildOfRelationship, isConnectsRelationship, RelationshipReference } from '@worldbrain/storex/lib/types' +import { + CollectionDefinition, + isRelationshipReference, + isChildOfRelationship, + isConnectsRelationship, + RelationshipReference, +} from '@worldbrain/storex/lib/types' export const getTermsIndex = (fieldName: string) => `_${fieldName}_terms` @@ -9,7 +17,9 @@ export function getDexieHistory(storageRegistry: StorageRegistry) { const versions: DexieSchema[] = [] let dexieVersion = 0 - for (const { collections: versionCollections } of storageRegistry.getSchemaHistory()) { + for (const { + collections: versionCollections, + } of storageRegistry.getSchemaHistory()) { Object.assign(collections, versionCollections) versions.push({ ...getDexieSchema(collections), @@ -37,16 +47,27 @@ function getDexieSchema(collections: RegistryCollections) { /** * Handles converting from StorageManager index definitions to Dexie index expressions. */ -function convertIndexToDexieExps({ name: collection, fields, indices, relationshipsByAlias }: CollectionDefinition) { +function convertIndexToDexieExps({ + name: collection, + fields, + indices, + relationshipsByAlias, +}: CollectionDefinition) { return (indices || []) .sort(({ pk }) => (pk ? -1 : 1)) // PK indexes always come first in Dexie - .map((indexDef) => { - const fieldNameFromRelationshipReference = (reference: RelationshipReference): string | string[] => { - const relationship = relationshipsByAlias![reference.relationship] + .map(indexDef => { + const fieldNameFromRelationshipReference = ( + reference: RelationshipReference, + ): string | string[] => { + const relationship = relationshipsByAlias![ + reference.relationship + ] if (!relationship) { throw new Error( `You tried to create an index in collection - '${collection}' on non-existing relationship '${reference.relationship}'` + '${collection}' on non-existing relationship '${ + reference.relationship + }'`, ) } @@ -55,7 +76,9 @@ function convertIndexToDexieExps({ name: collection, fields, indices, relationsh } else if (isConnectsRelationship(relationship)) { return relationship.fieldNames! } else { - throw new Error(`Unsupported relationship index in collection '${name}'`) + throw new Error( + `Unsupported relationship index in collection '${name}'`, + ) } } @@ -70,9 +93,13 @@ function convertIndexToDexieExps({ name: collection, fields, indices, relationsh const fieldNames = [] for (const field of source) { if (isRelationshipReference(field)) { - const fieldName = fieldNameFromRelationshipReference(field) + const fieldName = fieldNameFromRelationshipReference( + field, + ) if (fieldName instanceof Array) { - throw new Error(`Cannot create a compound index involving a 'connects' relationship`) + throw new Error( + `Cannot create a compound index involving a 'connects' relationship`, + ) } fieldNames.push(fieldName) } else { @@ -84,7 +111,10 @@ function convertIndexToDexieExps({ name: collection, fields, indices, relationsh // Create Dexie MultiEntry index for indexed text fields: http://dexie.org/docs/MultiEntry-Index // TODO: throw error if text field + PK index - if (!isRelationshipReference(source) && fields[source].type === 'text') { + if ( + !isRelationshipReference(source) && + fields[source].type === 'text' + ) { const fullTextField = indexDef.fullTextIndexName || getTermsIndex(indexDef.field as string) @@ -93,7 +123,12 @@ function convertIndexToDexieExps({ name: collection, fields, indices, relationsh // Note that order of these statements matters let listPrefix = indexDef.unique ? '&' : '' - listPrefix = indexDef.pk && (indexDef.autoInc || fields[indexDef.field as string].type === 'auto-pk') ? '++' : listPrefix + listPrefix = + indexDef.pk && + (indexDef.autoInc || + fields[indexDef.field as string].type === 'auto-pk') + ? '++' + : listPrefix return `${listPrefix}${indexDef.field}` }) diff --git a/ts/types.ts b/ts/types.ts index 63b1dfb..cb18a3d 100644 --- a/ts/types.ts +++ b/ts/types.ts @@ -1,18 +1,15 @@ import { Dexie } from 'dexie' -import { CollectionDefinition } from '@worldbrain/storex'; +import { CollectionDefinition } from '@worldbrain/storex' // import { FilterQuery as MongoFilterQuery } from 'mongodb' // tslint:disable-line export interface DexieMongoify extends Dexie { collection: ( name: string, ) => { - find(query : any): Dexie.Collection - count(query : any): Promise - update( - query : any, - update : any, - ): Promise<{ modifiedCount: number }> - remove(query : any): Promise<{ deletedCount: number }> + find(query: any): Dexie.Collection + count(query: any): Promise + update(query: any, update: any): Promise<{ modifiedCount: number }> + remove(query: any): Promise<{ deletedCount: number }> } } @@ -24,9 +21,13 @@ export interface DexieSchema { } } -export type UpdateOpApplier = (object : any, key: string, value: V) => void +export type UpdateOpApplier = ( + object: any, + key: string, + value: V, +) => void -export interface UpdateOps { +export interface UpdateOps { $inc: UpdateOpApplier $mul: UpdateOpApplier $rename: UpdateOpApplier @@ -43,9 +44,18 @@ export interface UpdateOps { $sort: UpdateOpApplier } -export type ObjectCleaner = (object : any, options : ObjectCleanerOptions) => Promise -export type ObjectCleanerOptions = { collectionDefinition : CollectionDefinition, stemmerSelector : StemmerSelector } +export type ObjectCleaner = ( + object: any, + options: ObjectCleanerOptions, +) => Promise +export type ObjectCleanerOptions = { + collectionDefinition: CollectionDefinition + stemmerSelector: StemmerSelector +} export type Stemmer = (text: string) => Set -export type StemmerSelector = (opts: { collectionName: string, fieldName: string }) => Stemmer | null +export type StemmerSelector = (opts: { + collectionName: string + fieldName: string +}) => Stemmer | null export type SchemaPatcher = (schema: DexieSchema[]) => DexieSchema[] diff --git a/ts/update-ops.ts b/ts/update-ops.ts index accde12..7af7dea 100644 --- a/ts/update-ops.ts +++ b/ts/update-ops.ts @@ -1,4 +1,4 @@ -import { UpdateOps } from "./types"; +import { UpdateOps } from './types' /** * Handles mutation of a document, updating each field in the way specified @@ -7,7 +7,7 @@ import { UpdateOps } from "./types"; * * TODO: Proper runtime error handling for badly formed update objs. */ -export function _processFieldUpdates(updates : any, object : any) { +export function _processFieldUpdates(updates: any, object: any) { // TODO: Find a home for this // TODO: Support all update ops const updateOpAppliers: UpdateOps = { @@ -30,8 +30,9 @@ export function _processFieldUpdates(updates : any, object : any) { for (const [updateKey, updateVal] of Object.entries(updates)) { // If supported update op, run assoc. update op applier if (updateOpAppliers[updateKey] != null) { - Object.entries(updateVal as any).forEach(([key, val] : [any, any]) => - updateOpAppliers[updateKey](object, key, val)) + Object.entries(updateVal as any).forEach(([key, val]: [any, any]) => + updateOpAppliers[updateKey](object, key, val), + ) } else { object[updateKey] = updateVal } diff --git a/ts/utils.ts b/ts/utils.ts index 058e3dc..12c4bda 100644 --- a/ts/utils.ts +++ b/ts/utils.ts @@ -1,7 +1,13 @@ -import { StorageRegistry, OperationBatch } from "@worldbrain/storex"; -import { dissectCreateObjectOperation, convertCreateObjectDissectionToBatch } from "@worldbrain/storex/lib/utils"; +import { StorageRegistry, OperationBatch } from '@worldbrain/storex' +import { + dissectCreateObjectOperation, + convertCreateObjectDissectionToBatch, +} from '@worldbrain/storex/lib/utils' -export function _flattenBatch(originalBatch : OperationBatch, registry : StorageRegistry) { +export function _flattenBatch( + originalBatch: OperationBatch, + registry: StorageRegistry, +) { const generatedBatch = [] let placeholdersGenerated = 0 const generatePlaceholder = () => `auto-gen:${++placeholdersGenerated}` @@ -23,7 +29,7 @@ export function _flattenBatch(originalBatch : OperationBatch, registry : Storage return generatePlaceholder() } } - })() + })(), }) const creationBatch = convertCreateObjectDissectionToBatch(dissection) for (const object of creationBatch) {