@journeyapps/db
Advanced tools
Comparing version 0.0.0-dev.362799e.d299e33 to 0.0.0-dev.375c2d3.0ce5033
/// <reference types="node" /> | ||
import 'isomorphic-fetch'; | ||
export declare class Attachment { | ||
static isAttachment(attachment: unknown): attachment is Attachment; | ||
id: string; | ||
@@ -5,0 +6,0 @@ state: string; |
@@ -1,13 +0,3 @@ | ||
import { Schema } from '@journeyapps/parser-schema'; | ||
import { Query } from './Query'; | ||
export interface ObjectData { | ||
id: string; | ||
type: string; | ||
attributes: { | ||
[index: string]: any; | ||
}; | ||
belongs_to: { | ||
[index: string]: string; | ||
}; | ||
} | ||
import { ObjectData } from './ObjectData'; | ||
export declare abstract class BaseAdapter { | ||
@@ -43,2 +33,3 @@ static logQuery(...args: any[]): any; | ||
getAll(type: string, ids: string[]): Promise<ObjectData[]>; | ||
count(query: Query): Promise<number>; | ||
/** | ||
@@ -52,29 +43,3 @@ * Destroy an object. | ||
ensureOpen(): void; | ||
updateSchemaIndexes(schema: Schema): Promise<void>; | ||
dataChanged(): void; | ||
getCrudBatch(limit?: number): Promise<{ | ||
crud: any[]; | ||
transaction: any; | ||
}>; | ||
crudComplete(transaction: any): Promise<any>; | ||
calculateHash(start: string, end: string): Promise<any>; | ||
clearRange(start: string, end: string, skip: any[]): Promise<any>; | ||
clearLocal(): Promise<any>; | ||
getLastUpdateId(): Promise<string>; | ||
/** | ||
* Range start and end must be defined (no open-ended ranges). | ||
* | ||
* Returns a list of start update id's for the ranges. Each range is closed by the next range's start UID, | ||
* and the last range is closed by the end parameter (not included in the results). | ||
* | ||
* This first number in the returned list will be the provided start value. | ||
* | ||
* The return list will have a minimum length of 1, and a maximum length of `split`. | ||
* | ||
* This function will attempt to split the range into `split` subranges, but with no subrange having | ||
* less than `minimum` objects, and each subrange having approximately the same number of objects. | ||
*/ | ||
splitRange(start: string, end: string, split: number, minimum: number): Promise<string[]>; | ||
explain(query: Query): Promise<any>; | ||
uidAtOffsets(start: string, end: string, offsets: number[]): Promise<string[]>; | ||
abstract getData(type: string, id: string): Promise<ObjectData>; | ||
@@ -81,0 +46,0 @@ abstract applyBatch(ops: any[]): Promise<any[]>; |
@@ -1,2 +0,4 @@ | ||
import { DatabaseObject, DatabaseAdapter, Database } from './database'; | ||
import { DatabaseObject } from './DatabaseObject'; | ||
import { DatabaseAdapter } from './DatabaseAdapter'; | ||
import { Database } from './Database'; | ||
export interface BatchError { | ||
@@ -17,2 +19,5 @@ object: DatabaseObject; | ||
} | ||
export interface ExecuteBatchResult { | ||
error?: any; | ||
} | ||
export declare class Batch { | ||
@@ -46,5 +51,4 @@ /** | ||
} | ||
export declare class CrudError { | ||
export declare class CrudError extends Error { | ||
errors: BatchError[]; | ||
message: string; | ||
constructor(errors: BatchError[]); | ||
@@ -54,3 +58,5 @@ /** | ||
*/ | ||
firstError: () => any; | ||
firstError(): { | ||
detail: string; | ||
}; | ||
} |
import { DatabaseObject } from './DatabaseObject'; | ||
import { ObjectType } from '@journeyapps/parser-schema'; | ||
import { DatabaseAdapter } from './database'; | ||
import { DatabaseAdapter } from './DatabaseAdapter'; | ||
import { Query } from './Query'; | ||
@@ -84,1 +84,2 @@ export declare class Collection<T extends DatabaseObject = DatabaseObject> { | ||
} | ||
export declare type TypedCollection<T> = Collection<DatabaseObject & T>; |
import { ObjectType } from '@journeyapps/parser-schema'; | ||
import { DatabaseAdapter } from './database'; | ||
export interface GenericObject { | ||
[key: string]: any; | ||
} | ||
import { DatabaseAdapter } from './DatabaseAdapter'; | ||
export declare class DatabaseObject { | ||
@@ -39,1 +36,4 @@ /** | ||
} | ||
export declare type CreateAccessor = (name: string, set: (name: string, value: any) => void, get: (name: string) => any) => void; | ||
export declare var createAccessor: CreateAccessor; | ||
export declare function setCreateAccessor(fn: CreateAccessor): void; |
@@ -1,29 +0,3 @@ | ||
import * as database from './database'; | ||
import * as evaluator from './evaluator'; | ||
import * as primitives from './primitives'; | ||
import * as query from './queryOperations'; | ||
import * as serializeInternal from './serializeInternal'; | ||
export { Day } from '@journeyapps/core-date'; | ||
/** | ||
* @deprecated | ||
*/ | ||
export { database, evaluator, primitives, query, serializeInternal }; | ||
export * from './credentials/ApiCredentials'; | ||
export * from './credentials/MobileCredentials'; | ||
export * from './Attachment'; | ||
export * from './BaseAdapter'; | ||
export * from './Batch'; | ||
export * from './Collection'; | ||
export * from './DatabaseObject'; | ||
export * from './DatabaseSet'; | ||
export * from './IndexedAdapter'; | ||
export * from './JourneyAPIAdapter'; | ||
export * from './Location'; | ||
export * from './ObjectRef'; | ||
export * from './browser'; | ||
export * from './OnlineDB'; | ||
export * from './Query'; | ||
export * from './WebSQLAdapter'; | ||
export * from './utils/base64'; | ||
export * from './utils/FunctionQueue'; | ||
export * from './utils/uuid'; | ||
export * from './database'; | ||
import './primitives'; |
import 'isomorphic-fetch'; | ||
import { BaseAdapter, ObjectData } from './BaseAdapter'; | ||
import { BaseAdapter } from './BaseAdapter'; | ||
import { Schema, ObjectType } from '@journeyapps/parser-schema'; | ||
import { Query } from './Query'; | ||
import { ExecuteBatchOperation } from './Batch'; | ||
import { ObjectData } from './ObjectData'; | ||
export interface ApiObjectData extends ObjectData { | ||
@@ -7,0 +8,0 @@ _updated_at?: string; |
import { ApiCredentialOptions } from './credentials/ApiCredentials'; | ||
import { Database } from './database'; | ||
import { Database } from './Database'; | ||
export interface OnlineDBCredentials extends ApiCredentialOptions { | ||
@@ -4,0 +4,0 @@ adapter?: any; |
@@ -1,14 +0,15 @@ | ||
import { DatabaseAdapter } from './database'; | ||
import { DatabaseAdapter } from './DatabaseAdapter'; | ||
import { ObjectType } from '@journeyapps/parser-schema'; | ||
import { Expression } from './queryOperations'; | ||
import { DatabaseObject } from './DatabaseObject'; | ||
import { FormatString } from '@journeyapps/evaluator'; | ||
interface RelationshipHash { | ||
[key: string]: RelationshipHash; | ||
} | ||
export declare class Query<T extends DatabaseObject = DatabaseObject> { | ||
static fromJSON(value: any, dbset: any): Query<DatabaseObject>; | ||
readonly type: ObjectType; | ||
private _freshness; | ||
private _include; | ||
constructor(adapter: DatabaseAdapter, type: ObjectType, expression?: Expression, ordering?: any[]); | ||
private _reload; | ||
private _explain; | ||
private _fetchWithDisplay; | ||
_fetchWithDisplay(columns?: FormatString[] | Record<string, FormatString>): Promise<T[]>; | ||
/** | ||
@@ -63,13 +64,2 @@ * Apply additional query filters, as a new query. | ||
private _count; | ||
toJSON(): { | ||
$query: { | ||
type: string; | ||
database: string; | ||
limit: number; | ||
expression: any; | ||
ordering: any[]; | ||
skip: number; | ||
_freshness: number; | ||
}; | ||
}; | ||
/** | ||
@@ -130,3 +120,17 @@ * Execute the query and return the results. | ||
orderBy(...fields: string[]): Query<T>; | ||
/** | ||
* For debugging purposes only. | ||
* | ||
* The format of the result is not defined. | ||
*/ | ||
toJSON(): { | ||
type: string; | ||
expression: Expression; | ||
ordering: any[]; | ||
limit: number; | ||
skip: number; | ||
include: RelationshipHash; | ||
}; | ||
private _includeInternal; | ||
} | ||
export {}; |
@@ -1,10 +0,8 @@ | ||
import { Type, Variable } from '@journeyapps/parser-schema'; | ||
import { ObjectData } from './BaseAdapter'; | ||
export declare function expressionFromJSON(scopeType: Type, json: any): Expression; | ||
export declare function expressionFromHash(scopeType: Type, hash: any): AndExpression; | ||
import { Variable, Type } from '@journeyapps/parser-schema'; | ||
import { ObjectData } from './ObjectData'; | ||
export declare function expressionFromHash(scopeType: Type, hash: any): Expression; | ||
export interface Expression { | ||
normalize(): OrExpression<AndExpression>; | ||
normalize(): NormalizedExpression; | ||
evaluate(object: ObjectData): boolean; | ||
toOriginalExpression(): any[]; | ||
toJSON(): any; | ||
} | ||
@@ -19,8 +17,2 @@ export declare class Operation implements Expression { | ||
evaluate(object: ObjectData): boolean; | ||
toJSON(): { | ||
__type: string; | ||
attribute: string; | ||
operator: string; | ||
value: any; | ||
}; | ||
toString(): string; | ||
@@ -35,7 +27,2 @@ toOriginalExpression(): any[]; | ||
evaluate(object: ObjectData): boolean; | ||
toJSON(): { | ||
__type: string; | ||
name: string; | ||
id: string; | ||
}; | ||
toString(): string; | ||
@@ -48,8 +35,4 @@ toOriginalExpression(): string[]; | ||
join(otherAnd: AndExpression): AndExpression; | ||
normalize(): OrExpression<AndExpression>; | ||
normalize(): any; | ||
evaluate(object: ObjectData): boolean; | ||
toJSON(): { | ||
__type: string; | ||
operands: any[]; | ||
}; | ||
toString(): string; | ||
@@ -63,25 +46,21 @@ toOriginalExpression(): any; | ||
evaluate(object: ObjectData): boolean; | ||
toJSON(): { | ||
__type: string; | ||
operands: any[]; | ||
}; | ||
toString(): string; | ||
toOriginalExpression(): any; | ||
} | ||
export declare type NormalizedExpression = OrExpression<AndExpression>; | ||
export declare class TrueExpression implements Expression { | ||
normalize(): OrExpression<AndExpression>; | ||
constructor(); | ||
normalize(): any; | ||
evaluate(object: ObjectData): boolean; | ||
toString(): string; | ||
toOriginalExpression(): string[]; | ||
toJSON(): { | ||
__type: string; | ||
}; | ||
} | ||
export declare class Tokenizer { | ||
expression: string; | ||
i: number; | ||
previousPosition: number; | ||
private interpolatorCount; | ||
private expression; | ||
constructor(expression: string); | ||
exception(message: string, start?: number, end?: number): Error; | ||
isWordSeparator(p: number): boolean; | ||
isWord(): boolean; | ||
@@ -95,8 +74,5 @@ hasMore(): boolean; | ||
readParenthesis(): string; | ||
readInterpolator(): number; | ||
readWord(): string; | ||
readInterpolator(): number; | ||
private advance; | ||
private isWordSeparator; | ||
private isLetter; | ||
private isDigit; | ||
private checkBuffer; | ||
@@ -107,5 +83,5 @@ private readToken; | ||
export declare class Parser { | ||
tokenizer: Tokenizer; | ||
scopeType: Type; | ||
args: any[]; | ||
private scopeType; | ||
private tokenizer; | ||
private args; | ||
constructor(scopeType: Type, tokenizer: Tokenizer, args: any[]); | ||
@@ -112,0 +88,0 @@ parse(): Expression; |
@@ -1,7 +0,12 @@ | ||
import { IndexSet } from './indexing/IndexSet'; | ||
import { IndexedAdapter, ExplainedResult } from './IndexedAdapter'; | ||
import { ObjectData } from './BaseAdapter'; | ||
import { IndexRange } from './indexing/IndexRange'; | ||
import { BaseAdapter } from './BaseAdapter'; | ||
import { Query } from './Query'; | ||
export declare class WebSQLAdapter extends IndexedAdapter { | ||
import { ObjectData } from './ObjectData'; | ||
export interface ExplainedResult { | ||
type: 'index' | 'full scan'; | ||
results: number; | ||
scanned: number; | ||
data: ObjectData[]; | ||
duration?: number; | ||
} | ||
export declare class WebSQLAdapter extends BaseAdapter { | ||
static adapterName: string; | ||
@@ -16,9 +21,2 @@ static supportsDiagnostics: boolean; | ||
description(): string; | ||
/** | ||
* Update the indexes to the specified set. | ||
* This runs in an exclusive lock, blocking all other database writes and all queries. | ||
* This operation is idempotent, and should be fast if all the indexes | ||
* are already up to date. | ||
*/ | ||
updateIndexes(indexes: IndexSet): Promise<any>; | ||
applyCrud(crud: any[]): Promise<{ | ||
@@ -34,3 +32,4 @@ updatedIds: string[]; | ||
applyBatch(crud: any[]): Promise<any[]>; | ||
saveData(data: ObjectData, patch: ObjectData, syncInfo?: any): Promise<void>; | ||
save(data: ObjectData, patch: ObjectData): Promise<any>; | ||
saveData(data: ObjectData, patch?: ObjectData): Promise<void>; | ||
/** | ||
@@ -41,35 +40,13 @@ * Destroy an object. | ||
*/ | ||
destroy(type: string, id: string, remoteChange?: boolean): Promise<void>; | ||
destroy(type: string, id: string): Promise<void>; | ||
/** | ||
* Perform a query. | ||
* Returns a promise that is resolved with the resultset. | ||
*/ | ||
executeQuery(query: Query): Promise<ObjectData[]>; | ||
explain(query: Query): Promise<ExplainedResult>; | ||
executeTableScan(query: Query): Promise<ExplainedResult>; | ||
getData(type: string, id: string): Promise<ObjectData>; | ||
getAll(type: string, ids: string[]): Promise<ObjectData[]>; | ||
getByIndex(range: IndexRange): Promise<ObjectData[]>; | ||
getByIndexes(typeName: string, ranges: IndexRange[]): Promise<ObjectData[]>; | ||
/** | ||
* Get a batch of objects to send to the server. | ||
* When the objects are successfully sent to the server, call crudComplete(t). | ||
* Returns a promise resolving with: {crud: [], transaction: t} | ||
*/ | ||
getCrudBatch(limit?: number): Promise<{ | ||
crud: any[]; | ||
transaction: any; | ||
}>; | ||
crudComplete(transaction: any): Promise<void>; | ||
/** | ||
* Delete all objects in the specified range, excluding those listed in the `skip` array (optional). | ||
*/ | ||
clearRange(start: string, end: string, skip: string[]): Promise<void>; | ||
clearLocal(): Promise<boolean>; | ||
/** | ||
* Sum the hashes of all objects in the specified range. | ||
* */ | ||
calculateHash(start: string, end: string): Promise<{ | ||
hash: any; | ||
count: any; | ||
}>; | ||
getLastUpdateId(): Promise<string>; | ||
uidAtOffset(start: string, end: string, offset: number): Promise<any>; | ||
uidAtOffsets(start: string, end: string, offsets: number[]): Promise<any[]>; | ||
estimateBytesUsed(): Promise<number>; | ||
estimateQueueBytesUsed(): Promise<number>; | ||
openDatabase(name: string): any; | ||
@@ -80,5 +57,3 @@ private promisedTransaction; | ||
private transactionDestroy; | ||
private _updateItemIndexes; | ||
private _updateIndexData; | ||
private logError; | ||
} |
import '@journeyapps/core-test-helpers'; | ||
import './CrudBatchSpec'; | ||
import './DatabaseAdapterSpec'; | ||
@@ -7,2 +6,1 @@ import './DatabaseSpec'; | ||
import './FunctionQueueSpec'; | ||
import './IndexKeySpec'; |
@@ -9,2 +9,11 @@ "use strict"; | ||
class Attachment { | ||
static isAttachment(attachment) { | ||
if (attachment instanceof Attachment) { | ||
return true; | ||
} | ||
else if (typeof attachment.id == 'string' && typeof attachment.present == 'function') { | ||
return true; | ||
} | ||
return false; | ||
} | ||
constructor(attrs) { | ||
@@ -11,0 +20,0 @@ attrs = attrs || {}; |
@@ -75,2 +75,6 @@ "use strict"; | ||
} | ||
async count(query) { | ||
const data = await this.executeQuery(query); | ||
return data.length; | ||
} | ||
/** | ||
@@ -82,3 +86,2 @@ * Destroy an object. | ||
destroy(type, id, dontLogCrud) { | ||
this.dataChanged(); | ||
return Promise.resolve(); | ||
@@ -94,87 +97,7 @@ } | ||
} | ||
// --- Indexing functionality --- | ||
updateSchemaIndexes(schema) { | ||
// no-op | ||
return Promise.resolve(); | ||
} | ||
// --- Sync functionality --- | ||
dataChanged() { | ||
// Called when data has been saved, to trigger sync. | ||
// Replace this function with one that can handle the change. | ||
} | ||
getCrudBatch(limit) { | ||
// It is up to implementations to override this to implement crud tracking. | ||
// With the default behaviour all tests should pass, but no crud is done. | ||
return Promise.resolve(null); | ||
} | ||
crudComplete(transaction) { | ||
return Promise.resolve(null); | ||
} | ||
// Sum the hashes of all objects in the specified range. | ||
// Both start and end are required. | ||
calculateHash(start, end) { | ||
return Promise.reject('Not implemented'); | ||
} | ||
// Delete all objects in the specified range, excluding those listed in the `skip` array (optional). | ||
clearRange(start, end, skip) { | ||
return Promise.resolve(null); | ||
} | ||
// Delete all objects without an uid. | ||
clearLocal() { | ||
return Promise.resolve(null); | ||
} | ||
// Return the highest update id in the database, as a promise. | ||
getLastUpdateId() { | ||
return Promise.reject('Not implemented'); | ||
} | ||
/** | ||
* Range start and end must be defined (no open-ended ranges). | ||
* | ||
* Returns a list of start update id's for the ranges. Each range is closed by the next range's start UID, | ||
* and the last range is closed by the end parameter (not included in the results). | ||
* | ||
* This first number in the returned list will be the provided start value. | ||
* | ||
* The return list will have a minimum length of 1, and a maximum length of `split`. | ||
* | ||
* This function will attempt to split the range into `split` subranges, but with no subrange having | ||
* less than `minimum` objects, and each subrange having approximately the same number of objects. | ||
*/ | ||
splitRange(start, end, split, minimum) { | ||
this.ensureOpen(); | ||
var self = this; | ||
start = start || ''; | ||
return this.calculateHash(start, end).then(function (result) { | ||
// We're not interested in the hash, only the count | ||
var count = result.count; | ||
var sectionSize = Math.max(Math.ceil(count / split), minimum); | ||
var numberOfSections = count / sectionSize; // number <= split | ||
var offsets = []; | ||
// We don't include the offset of 0 - we use `start` for this, so that the entire range is covered | ||
for (var i = 1; i < numberOfSections; i++) { | ||
offsets[i - 1] = i * sectionSize; | ||
} | ||
return self.uidAtOffsets(start, end, offsets).then(function (uids) { | ||
// We need to filter, in case the data has been modified since | ||
var filtered = [start]; | ||
var last = ''; | ||
for (var index = 0; index < uids.length; index++) { | ||
// Check that it is non-null, and strictly increasing | ||
if (uids[index] != null && uids[index] > last) { | ||
filtered.push(uids[index]); | ||
last = uids[index]; | ||
} | ||
} | ||
return filtered; | ||
}); | ||
}); | ||
} | ||
explain(query) { | ||
throw new Error('Method not implemented.'); | ||
} | ||
uidAtOffsets(start, end, offsets) { | ||
throw new Error('Method not implemented.'); | ||
} | ||
} | ||
exports.BaseAdapter = BaseAdapter; | ||
//# sourceMappingURL=BaseAdapter.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
function validateAdapter(batchAdapter, object) { | ||
if (object._adapter === batchAdapter || object._adapter.name == batchAdapter.name) { | ||
return; | ||
} | ||
throw new Error('Cannot add an object in ' + object._adapter.description() + ' to a Batch in ' + batchAdapter.description()); | ||
} | ||
class Batch { | ||
@@ -25,5 +31,3 @@ /** | ||
save(object) { | ||
if (object._adapter !== this.adapter) { | ||
throw new Error('Batch cannot save an object from a different database'); | ||
} | ||
validateAdapter(this.adapter, object); | ||
this._operations.push({ op: 'save', object: object }); | ||
@@ -35,5 +39,3 @@ } | ||
destroy(object) { | ||
if (object._adapter !== this.adapter) { | ||
throw new Error('Batch cannot save an object from a different database'); | ||
} | ||
validateAdapter(this.adapter, object); | ||
this._operations.push({ op: 'destroy', object: object }); | ||
@@ -49,2 +51,8 @@ } | ||
execute() { | ||
return this._execute(); | ||
} | ||
/** | ||
* @internal | ||
*/ | ||
_execute() { | ||
const ops = this._getOps(); | ||
@@ -165,4 +173,3 @@ if (ops.length == 0) { | ||
if (op.op == 'patch') { | ||
if (Object.keys(op.patch.attributes).length === 0 && | ||
Object.keys(op.patch.belongs_to).length === 0) { | ||
if (Object.keys(op.patch.attributes).length === 0 && Object.keys(op.patch.belongs_to).length === 0) { | ||
// No-op | ||
@@ -178,25 +185,20 @@ continue; | ||
exports.Batch = Batch; | ||
class CrudError { | ||
class CrudError extends Error { | ||
constructor(errors) { | ||
/** | ||
* Return the first error. Useful if this was used for a single operation. | ||
*/ | ||
this.firstError = function () { | ||
return this.errors[0].error; | ||
}; | ||
super(); | ||
this.errors = errors; | ||
let message = 'Crud operations failed.'; | ||
errors.forEach(function (error) { | ||
message += | ||
'\n ' + | ||
error.object.type.name + | ||
' ' + | ||
error.object.id + | ||
': ' + | ||
error.error.detail; | ||
message += '\n ' + error.object.type.name + ' ' + error.object.id + ': ' + error.error.detail; | ||
}); | ||
this.message = message; | ||
} | ||
/** | ||
* Return the first error. Useful if this was used for a single operation. | ||
*/ | ||
firstError() { | ||
return this.errors[0].error; | ||
} | ||
} | ||
exports.CrudError = CrudError; | ||
//# sourceMappingURL=Batch.js.map |
@@ -28,3 +28,3 @@ "use strict"; | ||
create(attributes) { | ||
var object = DatabaseObject_1.DatabaseObject.build(this.adapter, this.type); | ||
let object = DatabaseObject_1.DatabaseObject.build(this.adapter, this.type); | ||
object.setAll(attributes); | ||
@@ -39,15 +39,21 @@ return object; | ||
// TODO: varargs | ||
var self = this; | ||
if (arguments.length == 1 && typeof arguments[0] == 'string') { | ||
// Find by ID - return a placeholder | ||
var promise = this.adapter.get(this.type.name, id); | ||
return promise.then(function (data) { | ||
const promise = this.adapter.get(this.type.name, id); | ||
return promise.then(data => { | ||
if (data == null) { | ||
if (this.adapter.logWarnings) { | ||
console.warn('Could not find ' + this.type.name + ' with ' + id); | ||
} | ||
return null; | ||
} | ||
else { | ||
return DatabaseObject_1.DatabaseObject.build(self.adapter, self.type, id, data); | ||
return DatabaseObject_1.DatabaseObject.build(this.adapter, this.type, id, data); | ||
} | ||
}); | ||
} | ||
else if (arguments.length == 1 && arguments[0] == null) { | ||
// This can happen when passing in null/undefined as an ID. | ||
return Promise.reject('Query expression or ID required'); | ||
} | ||
else { | ||
@@ -54,0 +60,0 @@ // Same as a where query, but only return a single result. |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const parser_schema_1 = require("@journeyapps/parser-schema"); | ||
const database_1 = require("./database"); | ||
const Query_1 = require("./Query"); | ||
const Batch_1 = require("./Batch"); | ||
const queryOperations_1 = require("./queryOperations"); | ||
const uuid = require("uuid/v1"); | ||
const uuid = require("uuid/v4"); | ||
const evaluator_1 = require("@journeyapps/evaluator"); | ||
// type is the type object, not the name | ||
@@ -20,3 +21,3 @@ class DatabaseObject { | ||
constructor(adapter, type, id) { | ||
if (adapter == null || typeof adapter.save != 'function') { | ||
if (adapter == null || typeof adapter.getData != 'function') { | ||
throw new Error('adapter is required'); | ||
@@ -27,9 +28,9 @@ } | ||
} | ||
var objself = this; | ||
const objself = this; | ||
// This is where all the attributes are stored | ||
var attributes = {}; | ||
const attributes = {}; | ||
// Attributes that have been set by the user. | ||
var attributesDirty = {}; | ||
let attributesDirty = {}; | ||
// belongsToIds is only used when there is no corresponding object in belongsToCache. | ||
var belongsToIds = {}; | ||
const belongsToIds = {}; | ||
// belongsToCache serves as a cache for relationships. The id in belongsToIds should always correspond to | ||
@@ -39,13 +40,13 @@ // the id here, if the object is present. | ||
// means that we haven't queried yet. | ||
var belongsToCache = {}; | ||
let belongsToCache = {}; | ||
// Relationships that have been set by the user. | ||
// true means set by user, false or not present means not set | ||
var belongsToDirty = {}; | ||
let belongsToDirty = {}; | ||
// We keep track of what we are currently loading, so that we don't execute the same query many times | ||
// in succession. | ||
var belongsToLoading = {}; | ||
const belongsToLoading = {}; | ||
// Caching the queries prevents them from being re-executed all the time. | ||
var hasManyCache = {}; | ||
var persisted; | ||
var destroyed = false; | ||
let hasManyCache = {}; | ||
let persisted; | ||
let destroyed = false; | ||
if (id == null) { | ||
@@ -91,9 +92,9 @@ // New object | ||
if (data != null) { | ||
var resolveAttributes = data.attributes || {}; | ||
const resolveAttributes = data.attributes || {}; | ||
Object.keys(type.attributes).forEach(function (key) { | ||
var attribute = type.attributes[key]; | ||
var value = resolveAttributes[key]; | ||
const attribute = type.attributes[key]; | ||
const value = resolveAttributes[key]; | ||
// We don't override any attributes that have already been set. | ||
if (!attributesDirty[key]) { | ||
var realValue = value == null ? null : attribute.type.valueFromJSON(value); | ||
const realValue = value == null ? null : attribute.type.valueFromJSON(value); | ||
// We use the setter so that type checking occurs... | ||
@@ -135,3 +136,3 @@ try { | ||
if (values != null) { | ||
for (var key in values) { | ||
for (let key in values) { | ||
if (values.hasOwnProperty(key)) { | ||
@@ -143,3 +144,3 @@ _set(key, values[key]); | ||
} | ||
var saving = false; | ||
let saving = false; | ||
// Returns a promise that is resolved when this object is saved. | ||
@@ -154,5 +155,5 @@ function _save() { | ||
// belongs-to relationships. | ||
var batch = new Batch_1.Batch(adapter); | ||
let batch = new Batch_1.Batch(adapter); | ||
batch.save(this); | ||
return batch.execute().catch(function (error) { | ||
return batch._execute().catch(function (error) { | ||
if (error instanceof Batch_1.CrudError) { | ||
@@ -203,3 +204,3 @@ return Promise.reject(error.firstError()); | ||
Object.keys(belongsToCache).forEach(function (key) { | ||
var related = belongsToCache[key]; | ||
const related = belongsToCache[key]; | ||
if (related != null) { | ||
@@ -238,7 +239,7 @@ if (related._adapter === adapter) { | ||
var displayFormat = type.displayFormat; | ||
return displayFormat.evaluate(objself); | ||
return displayFormat.evaluate(new evaluator_1.VariableFormatStringScope(objself)); | ||
} | ||
function toData(patch) { | ||
// We don't modify belongToIds here - this must be a read-only method. | ||
var jsonBelongsTo = {}; | ||
let jsonBelongsTo = {}; | ||
Object.keys(type.belongsTo).forEach(function (name) { | ||
@@ -257,6 +258,6 @@ let belongsToId = belongsToIds[name]; | ||
}); | ||
var jsonAttributes = {}; | ||
let jsonAttributes = {}; | ||
Object.keys(type.attributes).forEach(function (name) { | ||
var attribute = type.attributes[name]; | ||
var value = attributes[name]; | ||
const attribute = type.attributes[name]; | ||
const value = attributes[name]; | ||
if (patch) { | ||
@@ -286,5 +287,5 @@ if (attributesDirty[name]) { | ||
// Getters and setters for attributes | ||
var blacklist = ['save', 'id', '_database', 'reload']; | ||
const blacklist = ['save', 'id', '_database', 'reload']; | ||
function setAttribute(name, value) { | ||
var attribute = type.attributes[name]; | ||
const attribute = type.attributes[name]; | ||
if (attribute == null) { | ||
@@ -343,3 +344,3 @@ return; | ||
function triggerLoad(name) { | ||
var rel = type.belongsTo[name]; | ||
const rel = type.belongsTo[name]; | ||
if (rel == null) { | ||
@@ -356,3 +357,3 @@ return 'nothing to load'; | ||
else { | ||
getBelongsTo(name); | ||
const _ignorePromise = getBelongsTo(name); | ||
return 'loading'; | ||
@@ -366,3 +367,3 @@ } | ||
function getBelongsTo(name) { | ||
var rel = type.belongsTo[name]; | ||
const rel = type.belongsTo[name]; | ||
if (rel == null) { | ||
@@ -379,7 +380,7 @@ return Promise.resolve(undefined); | ||
else { | ||
var foreignType = rel.foreignType; | ||
var relatedId = belongsToIds[name]; | ||
var promise = adapter.getData(foreignType.name, relatedId); | ||
var promise2 = promise.then(function (data) { | ||
var obj = null; | ||
const foreignType = rel.foreignType; | ||
const relatedId = belongsToIds[name]; | ||
const promise = adapter.getData(foreignType.name, relatedId); | ||
const promise2 = promise.then(function (data) { | ||
let obj = null; | ||
if (data) { | ||
@@ -410,3 +411,3 @@ obj = DatabaseObject.build(adapter, foreignType, relatedId, data); | ||
} | ||
var accessor = database_1.createAccessor(name, setBelongsTo, getBelongsTo); | ||
var accessor = exports.createAccessor(name, setBelongsTo, getBelongsTo); | ||
if (accessor != null) { | ||
@@ -449,3 +450,3 @@ Object.defineProperty(objself, name, { | ||
Object.keys(type.hasMany).forEach(function (name) { | ||
var rel = type.hasMany[name]; | ||
const rel = type.hasMany[name]; | ||
if (blacklist.indexOf(name) != -1) { | ||
@@ -460,4 +461,4 @@ return; | ||
// This does not cache the results - widgets are responsible for that. | ||
var expr = new queryOperations_1.RelationMatch(rel.name, objself.id); | ||
hasManyCache[name] = new database_1.Query(adapter, rel.objectType, expr); | ||
const expr = new queryOperations_1.RelationMatch(rel.name, objself.id); | ||
hasManyCache[name] = new Query_1.Query(adapter, rel.objectType, expr); | ||
} | ||
@@ -474,3 +475,3 @@ return hasManyCache[name]; | ||
if (name === 'id') { | ||
return id; | ||
return Promise.resolve(id); | ||
} | ||
@@ -534,7 +535,7 @@ else if (type.attributes[name]) { | ||
} | ||
var data = toData(false); // TODO: should we convert to JSON and back? | ||
const data = toData(false); // TODO: should we convert to JSON and back? | ||
// If the object is not persisted, we generate a create an object | ||
// without an id. Otherwise the clone thinks it is persisted. | ||
var newId = persisted ? id : null; | ||
var clone = new DatabaseObject(adapter, newType, newId); | ||
const newId = persisted ? id : null; | ||
let clone = new DatabaseObject(adapter, newType, newId); | ||
clone.resolve(data); | ||
@@ -544,4 +545,4 @@ return clone; | ||
function _display() { | ||
var displayFormat = type.displayFormat; | ||
return displayFormat.evaluatePromise(objself); | ||
const displayFormat = type.displayFormat; | ||
return displayFormat.evaluatePromise(new evaluator_1.VariableFormatStringScope(objself)); | ||
} | ||
@@ -620,2 +621,17 @@ // public functions, but not enumerable | ||
} | ||
exports.createAccessor = (name, set, get) => { | ||
return (value) => { | ||
if (typeof value != 'undefined') { | ||
return set(name, value); | ||
} | ||
else { | ||
return get(name); | ||
} | ||
}; | ||
}; | ||
// For internal use only | ||
function setCreateAccessor(fn) { | ||
exports.createAccessor = fn; | ||
} | ||
exports.setCreateAccessor = setCreateAccessor; | ||
//# sourceMappingURL=DatabaseObject.js.map |
@@ -6,34 +6,6 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const database = require("./database"); | ||
exports.database = database; | ||
const evaluator = require("./evaluator"); | ||
exports.evaluator = evaluator; | ||
const primitives = require("./primitives"); | ||
exports.primitives = primitives; | ||
const query = require("./queryOperations"); | ||
exports.query = query; | ||
const serializeInternal = require("./serializeInternal"); | ||
exports.serializeInternal = serializeInternal; | ||
// Convenience export | ||
var core_date_1 = require("@journeyapps/core-date"); | ||
exports.Day = core_date_1.Day; | ||
__export(require("./credentials/ApiCredentials")); | ||
__export(require("./credentials/MobileCredentials")); | ||
__export(require("./Attachment")); | ||
__export(require("./BaseAdapter")); | ||
__export(require("./Batch")); | ||
__export(require("./Collection")); | ||
__export(require("./DatabaseObject")); | ||
__export(require("./DatabaseSet")); | ||
__export(require("./IndexedAdapter")); | ||
__export(require("./JourneyAPIAdapter")); | ||
__export(require("./Location")); | ||
__export(require("./ObjectRef")); | ||
// Everything from browser.ts, as well as OnlineDB | ||
__export(require("./browser")); | ||
__export(require("./OnlineDB")); | ||
__export(require("./Query")); | ||
__export(require("./WebSQLAdapter")); | ||
__export(require("./utils/base64")); | ||
__export(require("./utils/FunctionQueue")); | ||
__export(require("./utils/uuid")); | ||
__export(require("./database")); | ||
require("./primitives"); | ||
//# sourceMappingURL=index.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
require("isomorphic-fetch"); | ||
const retryableFetch_1 = require("./utils/retryableFetch"); | ||
const BaseAdapter_1 = require("./BaseAdapter"); | ||
@@ -218,3 +219,3 @@ const parser_schema_1 = require("@journeyapps/parser-schema"); | ||
async loadDataModel() { | ||
const response = await fetch(this.credentials.api4Url() + 'datamodel.xml', { | ||
const response = await retryableFetch_1.retryableFetch(this.credentials.api4Url() + 'datamodel.xml', { | ||
headers: { | ||
@@ -239,3 +240,6 @@ Accept: 'application/xml', | ||
}; | ||
return fetch(url, Object.assign({}, this.options.fetch, getOptions)).then(responseHandler); | ||
return retryableFetch_1.retryableFetch(url, { | ||
...this.options.fetch, | ||
...getOptions | ||
}).then(responseHandler); | ||
} | ||
@@ -251,3 +255,6 @@ apiPost(url, data) { | ||
}; | ||
return fetch(url, Object.assign({}, this.options.fetch, postOptions)).then(responseHandler); | ||
return retryableFetch_1.retryableFetch(url, { | ||
...this.options.fetch, | ||
...postOptions | ||
}).then(responseHandler); | ||
} | ||
@@ -275,3 +282,3 @@ /** | ||
// empty query expressions - eg. DB.user.all() - must use the standard API 'retrieve all objects' method. | ||
if (typeof queryExpression == 'undefined') { | ||
if (typeof queryExpression == 'undefined' || queryExpression == '') { | ||
url = this.credentials.api4Url() + 'objects/' + typeName + '.json'; | ||
@@ -278,0 +285,0 @@ var limitUrl = 'limit=' + |
@@ -41,15 +41,4 @@ "use strict"; | ||
} | ||
/** @internal */ | ||
toInternalJSON() { | ||
return { | ||
latitude: this.latitude, | ||
longitude: this.longitude, | ||
altitude: this.altitude, | ||
horizontal_accuracy: this.horizontal_accuracy, | ||
vertical_accuracy: this.vertical_accuracy, | ||
timestamp: this.timestamp.toISOString() | ||
}; | ||
} | ||
} | ||
exports.Location = Location; | ||
//# sourceMappingURL=Location.js.map |
@@ -7,14 +7,14 @@ "use strict"; | ||
const ApiCredentials_1 = require("./credentials/ApiCredentials"); | ||
const database_1 = require("./database"); | ||
const Database_1 = require("./Database"); | ||
function OnlineDB(options) { | ||
var fs = require('fs'); | ||
var datamodelXml = fs.readFileSync(options.dataModelPath, 'utf8'); | ||
var schema = new parser_schema_1.Schema().loadXml(datamodelXml, { | ||
const fs = require('fs'); | ||
const datamodelXml = fs.readFileSync(options.dataModelPath, 'utf8'); | ||
const schema = new parser_schema_1.Schema().loadXml(datamodelXml, { | ||
apiVersion: new parser_common_1.Version('4.0') | ||
}); | ||
var credentials = new ApiCredentials_1.ApiCredentials(options); | ||
var adapter = new JourneyAPIAdapter_1.JourneyAPIAdapter(credentials, schema, options.adapter); | ||
return new database_1.Database(schema, adapter); | ||
const credentials = new ApiCredentials_1.ApiCredentials(options); | ||
const adapter = new JourneyAPIAdapter_1.JourneyAPIAdapter(credentials, schema, options.adapter); | ||
return new Database_1.Database(schema, adapter); | ||
} | ||
exports.OnlineDB = OnlineDB; | ||
//# sourceMappingURL=OnlineDB.js.map |
@@ -319,3 +319,3 @@ "use strict"; | ||
parser_schema_1.primitives.attachment.prototype.cast = function (value) { | ||
if (value instanceof Attachment_1.Attachment) { | ||
if (Attachment_1.Attachment.isAttachment(value)) { | ||
return value; | ||
@@ -322,0 +322,0 @@ } |
@@ -26,2 +26,3 @@ "use strict"; | ||
this.skipNumber = 0; | ||
// Relationship structure to preload. | ||
this._include = {}; | ||
@@ -43,17 +44,2 @@ // Watch this variable for changes to determine when the query should be reloaded. | ||
} | ||
static fromJSON(value, dbset) { | ||
const q = value.$query; | ||
if (dbset == null) { | ||
throw new Error('Database is required to load query'); | ||
} | ||
else if (dbset.schema == null) { | ||
throw new Error('Schema is required to load query'); | ||
} | ||
const objectType = dbset.schema.objects[q.type]; | ||
const adapter = dbset.adapters[q.database]; | ||
const qq = new Query(adapter, objectType, queryOperations_1.expressionFromJSON(objectType, q.expression), q.ordering); | ||
qq.limitNumber = q.limit; | ||
qq.skipNumber = q.skip; | ||
return qq; | ||
} | ||
/** @internal */ | ||
@@ -88,8 +74,8 @@ _clone(newType) { | ||
_sort(objects) { | ||
var ordering = this.ordering; | ||
const ordering = this.ordering; | ||
if (ordering.length > 0) { | ||
objects.sort(function (a, b) { | ||
for (var i = 0; i < ordering.length; i++) { | ||
var attribute = ordering[i]; | ||
var ascending = 1; | ||
for (let i = 0; i < ordering.length; i++) { | ||
let attribute = ordering[i]; | ||
let ascending = 1; | ||
if (attribute[0] == '-') { | ||
@@ -99,5 +85,5 @@ ascending = -1; | ||
} | ||
var valueA = a.attributes[attribute]; | ||
var valueB = b.attributes[attribute]; | ||
var diff = compare(valueA, valueB); | ||
const valueA = a.attributes[attribute]; | ||
const valueB = b.attributes[attribute]; | ||
const diff = compare(valueA, valueB); | ||
if (diff !== 0) { | ||
@@ -107,3 +93,5 @@ return diff * ascending; | ||
} | ||
return 0; | ||
const idDiff = compare(a.id, b.id); | ||
// Do final sorting by ID to make it deterministic. | ||
return idDiff; | ||
}); | ||
@@ -117,5 +105,5 @@ } | ||
var result = []; | ||
for (var i = 0; i < data.length; i++) { | ||
var objdata = data[i]; | ||
var object = DatabaseObject_1.DatabaseObject.build(this.adapter, this.type, objdata.id, objdata); | ||
for (let i = 0; i < data.length; i++) { | ||
const objdata = data[i]; | ||
const object = DatabaseObject_1.DatabaseObject.build(this.adapter, this.type, objdata.id, objdata); | ||
result.push(object); | ||
@@ -162,5 +150,5 @@ } | ||
where(filters, ...args) { | ||
var cloned = this._clone(); | ||
const cloned = this._clone(); | ||
if (arguments.length > 0) { | ||
var e; | ||
let e; | ||
if (typeof filters == 'object') { | ||
@@ -174,3 +162,3 @@ // example: where({make: 'Nokia', model: '5800}) | ||
} | ||
var combined = new queryOperations_1.AndExpression([this.expression, e]); | ||
const combined = new queryOperations_1.AndExpression([this.expression, e]); | ||
cloned.expression = combined; | ||
@@ -180,5 +168,5 @@ } | ||
} | ||
order_by() { | ||
var attributes = Array.prototype.slice.call(arguments); | ||
var q = this._clone(); | ||
order_by(...fields) { | ||
const attributes = Array.prototype.slice.call(fields); | ||
let q = this._clone(); | ||
q.ordering = attributes; | ||
@@ -195,3 +183,3 @@ return q; | ||
limit(n) { | ||
var q = this._clone(); | ||
let q = this._clone(); | ||
q.limitNumber = n; | ||
@@ -208,3 +196,3 @@ return q; | ||
skip(n) { | ||
var q = this._clone(); | ||
let q = this._clone(); | ||
q.skipNumber = n; | ||
@@ -234,27 +222,12 @@ return q; | ||
return this._fetch().then((objects) => { | ||
var batch = new Batch_1.Batch(this.adapter); | ||
let batch = new Batch_1.Batch(this.adapter); | ||
objects.forEach(function (object) { | ||
batch.destroy(object); | ||
}); | ||
return batch.execute(); | ||
return batch._execute(); | ||
}); | ||
} | ||
_count() { | ||
return this.adapter.executeQuery(this).then(function (data) { | ||
return data.length; | ||
}); | ||
return this.adapter.count(this); | ||
} | ||
toJSON() { | ||
return { | ||
$query: { | ||
type: this.type.name, | ||
database: this.adapter.name, | ||
limit: this.limitNumber, | ||
expression: this.expression.toJSON(), | ||
ordering: this.ordering, | ||
skip: this.skipNumber, | ||
_freshness: this._freshness | ||
} | ||
}; | ||
} | ||
// Aliases | ||
@@ -314,4 +287,19 @@ /** | ||
} | ||
/** | ||
* For debugging purposes only. | ||
* | ||
* The format of the result is not defined. | ||
*/ | ||
toJSON() { | ||
return { | ||
type: this.type.name, | ||
expression: this.expression, | ||
ordering: this.ordering, | ||
limit: this.limitNumber, | ||
skip: this.skipNumber, | ||
include: this._include | ||
}; | ||
} | ||
_includeInternal(relationshipHash) { | ||
var q = this._clone(); | ||
let q = this._clone(); | ||
deepMerge(q._include, relationshipHash); | ||
@@ -328,5 +316,17 @@ return q; | ||
} | ||
if (a > b) { | ||
// We can't > or < with `null` or `undefined` - do this manually. | ||
// null, undefined < anything else | ||
// Note that `== null` also matches `undefined`. | ||
if (a == null && b == null) { | ||
return 0; | ||
} | ||
else if (a == null) { | ||
return -1; | ||
} | ||
else if (b == null) { | ||
return 1; | ||
} | ||
else if (a > b) { | ||
return 1; | ||
} | ||
else if (a < b) { | ||
@@ -360,3 +360,3 @@ return -1; | ||
// compared to querying the database. | ||
var relationships = {}; | ||
let relationships = {}; | ||
if (columns == null) { | ||
@@ -368,5 +368,17 @@ // object-list, use the default display format for the object | ||
// object-table, use the specified column values | ||
for (let display of columns) { | ||
deepMerge(relationships, display.extractRelationshipStructure(type)); | ||
if (Array.isArray(columns)) { | ||
for (let display of columns) { | ||
if (display && typeof display.extractRelationshipStructure == 'function') { | ||
deepMerge(relationships, display.extractRelationshipStructure(type)); | ||
} | ||
} | ||
} | ||
else { | ||
for (let key in columns) { | ||
const display = columns[key]; | ||
if (display && typeof display.extractRelationshipStructure == 'function') { | ||
deepMerge(relationships, display.extractRelationshipStructure(type)); | ||
} | ||
} | ||
} | ||
} | ||
@@ -408,9 +420,9 @@ return relationships; | ||
} | ||
var nestedObjects = []; | ||
let nestedObjects = []; | ||
const promise = adapter.getAll(relatedType.name, ids).then(async (data) => { | ||
for (let i = 0; i < objects.length; i++) { | ||
var object = objects[i]; | ||
var relatedData = data[i]; | ||
const object = objects[i]; | ||
const relatedData = data[i]; | ||
if (relatedData != null) { | ||
var related = DatabaseObject_1.DatabaseObject.build(adapter, relatedType, relatedData.id, relatedData); | ||
const related = DatabaseObject_1.DatabaseObject.build(adapter, relatedType, relatedData.id, relatedData); | ||
object._cache(key, related); | ||
@@ -417,0 +429,0 @@ nestedObjects.push(related); |
@@ -7,70 +7,11 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const primitives = require("./primitives"); | ||
const internal = require("./serializeInternal"); | ||
const parser_schema_1 = require("@journeyapps/parser-schema"); | ||
var _flatten = require('lodash/flatten'); // tslint:disable-line | ||
var _tail = require('lodash/tail'); // tslint:disable-line | ||
var Day = primitives.Day; | ||
parser_schema_1.QueryType.prototype.cast = function (value) { | ||
if (typeof value != 'object') { | ||
throw new Error(value + ' is not an object'); | ||
} | ||
var thisTypeName = this.objectType.name; | ||
if (value.type != null && | ||
value.type instanceof parser_schema_1.ObjectType && | ||
value.type.name == thisTypeName && | ||
typeof value._fetch == 'function') { | ||
// This implies that value is (likely) also an instance of Query. | ||
return value; | ||
} | ||
else { | ||
// We do not print the value here, since it may be a massive array. | ||
throw new Error('Expected value to have query type ' + thisTypeName); | ||
} | ||
}; | ||
// Parse a serialized expression | ||
function expressionFromJSON(scopeType, json) { | ||
if (json == null) { | ||
return new TrueExpression(); | ||
} | ||
var expressionType = json.__type; | ||
if (expressionType == 'Operation') { | ||
const attribute = scopeType.getAttribute(json.attribute); | ||
if (attribute == null) { | ||
throw new Error("'" + json.attribute + "' is not defined on '" + scopeType.name + "'"); | ||
} | ||
const value = internal.deserialize(json.value); | ||
return new Operation(attribute, json.operator, value); | ||
} | ||
else if (expressionType == 'TrueExpression') { | ||
return new TrueExpression(); | ||
} | ||
else if (expressionType == 'AndExpression' || | ||
expressionType == 'OrExpression') { | ||
var operands = []; | ||
for (let value of json.operands) { | ||
operands.push(expressionFromJSON(scopeType, value)); | ||
} | ||
if (expressionType == 'AndExpression') { | ||
return new AndExpression(operands); | ||
} | ||
else { | ||
return new OrExpression(operands); | ||
} | ||
} | ||
else if (expressionType == 'RelationMatch') { | ||
return new RelationMatch(json.name, json.id); | ||
} | ||
else { | ||
throw new Error('Unknown expression: ' + JSON.stringify(json)); | ||
} | ||
} | ||
exports.expressionFromJSON = expressionFromJSON; | ||
const core_date_1 = require("@journeyapps/core-date"); | ||
const _ = require("lodash"); | ||
// Build an expression from a hash such as {make: 'Nokia', model: '5800'} | ||
function expressionFromHash(scopeType, hash) { | ||
var operations = []; | ||
for (var key in hash) { | ||
let operations = []; | ||
for (let key in hash) { | ||
if (hash.hasOwnProperty(key)) { | ||
var value = hash[key]; | ||
var attribute = scopeType.getAttribute(key); | ||
let value = hash[key]; | ||
const attribute = scopeType.getAttribute(key); | ||
if (attribute == null) { | ||
@@ -82,3 +23,3 @@ throw new Error("'" + key + "' is not defined on '" + scopeType.name + "'"); | ||
} | ||
var operation = new Operation(attribute, '=', value); | ||
const operation = new Operation(attribute, '=', value); | ||
operations.push(operation); | ||
@@ -92,2 +33,5 @@ } | ||
// An operation has an attribute (variable), operator and (constant) value to compare with. | ||
function discardMillis(ms) { | ||
return Math.floor(ms / 1000) * 1000; | ||
} | ||
class Operation { | ||
@@ -106,4 +50,4 @@ constructor(attribute, operator, value) { | ||
evaluate(object) { | ||
var rawAttributeValue; | ||
var attributeValue; | ||
let rawAttributeValue; | ||
let attributeValue; | ||
if (this.attribute.relationship) { | ||
@@ -126,10 +70,22 @@ attributeValue = object.belongs_to[this.attribute.relationship]; | ||
} | ||
var compareDiff = null; | ||
let compareDiff = null; | ||
if (typeof attributeValue == 'number' && typeof this.value == 'number') { | ||
compareDiff = attributeValue - this.value; | ||
} | ||
else if (typeof attributeValue == 'string' && typeof this.value == 'string') { | ||
if (attributeValue < this.value) { | ||
compareDiff = -1; | ||
} | ||
else if (attributeValue > this.value) { | ||
compareDiff = 1; | ||
} | ||
else { | ||
compareDiff = 0; | ||
} | ||
} | ||
else if (attributeValue instanceof Date && this.value instanceof Date) { | ||
compareDiff = attributeValue.getTime() - this.value.getTime(); | ||
// Compare seconds only | ||
compareDiff = discardMillis(attributeValue.getTime()) - discardMillis(this.value.getTime()); | ||
} | ||
else if (attributeValue instanceof Day || this.value instanceof Day) { | ||
else if (attributeValue instanceof core_date_1.Day || this.value instanceof core_date_1.Day) { | ||
// New syntax only | ||
@@ -139,4 +95,3 @@ try { | ||
// Also works for JSON strings. | ||
compareDiff = | ||
new Day(attributeValue).valueOf() - new Day(this.value).valueOf(); | ||
compareDiff = new core_date_1.Day(attributeValue).valueOf() - new core_date_1.Day(this.value).valueOf(); | ||
} | ||
@@ -185,3 +140,3 @@ catch (err) { | ||
if (typeof attributeValue == 'string' && typeof this.value == 'string') { | ||
return (attributeValue.toLowerCase().indexOf(this.value.toLowerCase()) >= 0); | ||
return attributeValue.toLowerCase().indexOf(this.value.toLowerCase()) >= 0; | ||
} | ||
@@ -194,3 +149,3 @@ else { | ||
if (typeof attributeValue == 'string' && typeof this.value == 'string') { | ||
return (attributeValue.toLowerCase().indexOf(this.value.toLowerCase()) === 0); | ||
return attributeValue.toLowerCase().indexOf(this.value.toLowerCase()) === 0; | ||
} | ||
@@ -205,21 +160,9 @@ else { | ||
} | ||
toJSON() { | ||
return { | ||
__type: 'Operation', | ||
attribute: this.attribute.name, | ||
operator: this.operator, | ||
value: internal.serialize(this.value) | ||
}; | ||
} | ||
toString() { | ||
return this.attribute.name + ' ' + this.operator + ' ' + this.value; | ||
} | ||
// This is relevant only for the Backend JS Console | ||
toOriginalExpression() { | ||
if (this.attribute.relationship) { | ||
// Works for both relationships (user = ?) and relationship ids (user_id = ?) | ||
return [ | ||
this.attribute.relationship + '_id ' + this.operator + ' ?', | ||
this.value | ||
]; | ||
return [this.attribute.relationship + '_id ' + this.operator + ' ?', this.value]; | ||
} | ||
@@ -243,16 +186,8 @@ else { | ||
evaluate(object) { | ||
var relatedId = object.belongs_to[this.name]; | ||
const relatedId = object.belongs_to[this.name]; | ||
return relatedId == this.id; | ||
} | ||
toJSON() { | ||
return { | ||
__type: 'RelationMatch', | ||
name: this.name, | ||
id: this.id | ||
}; | ||
} | ||
toString() { | ||
return this.name + ' = ' + this.id; | ||
} | ||
// This is relevant only for the Backend JS Console | ||
toOriginalExpression() { | ||
@@ -270,11 +205,11 @@ return [this.name + '_id = ?', this.id]; | ||
join(otherAnd) { | ||
var ops = this.operands.concat(otherAnd.operands); | ||
const ops = this.operands.concat(otherAnd.operands); | ||
return new AndExpression(ops); | ||
} | ||
normalize() { | ||
var i; | ||
let i; | ||
// Step 1: filter out TrueExpression | ||
var operands = []; | ||
let operands = []; | ||
for (i = 0; i < this.operands.length; i++) { | ||
var op = this.operands[i]; | ||
const op = this.operands[i]; | ||
if (!(op instanceof TrueExpression)) { | ||
@@ -291,12 +226,12 @@ operands.push(op); | ||
else if (operands.length > 2) { | ||
var a = new AndExpression(operands.slice(0, 2)); | ||
var b = new AndExpression(operands.slice(2)); | ||
const a = new AndExpression(operands.slice(0, 2)); | ||
const b = new AndExpression(operands.slice(2)); | ||
return new AndExpression([a, b]).normalize(); | ||
} | ||
else { | ||
var left = operands[0].normalize().operands; | ||
var right = operands[1].normalize().operands; | ||
var newOperands = []; | ||
const left = operands[0].normalize().operands; | ||
const right = operands[1].normalize().operands; | ||
let newOperands = []; | ||
for (i = 0; i < left.length; i++) { | ||
for (var j = 0; j < right.length; j++) { | ||
for (let j = 0; j < right.length; j++) { | ||
newOperands.push(left[i].join(right[j])); | ||
@@ -309,4 +244,4 @@ } | ||
evaluate(object) { | ||
for (var i = 0; i < this.operands.length; i++) { | ||
var operand = this.operands[i]; | ||
for (let i = 0; i < this.operands.length; i++) { | ||
const operand = this.operands[i]; | ||
if (!operand.evaluate(object)) { | ||
@@ -318,14 +253,6 @@ return false; | ||
} | ||
toJSON() { | ||
return { | ||
__type: 'AndExpression', | ||
operands: this.operands.map(function (o) { | ||
return o.toJSON(); | ||
}) | ||
}; | ||
} | ||
toString() { | ||
var s = '('; | ||
for (var i = 0; i < this.operands.length; i++) { | ||
var operand = this.operands[i]; | ||
let s = '('; | ||
for (let i = 0; i < this.operands.length; i++) { | ||
const operand = this.operands[i]; | ||
s += operand.toString(); | ||
@@ -339,21 +266,27 @@ if (i != this.operands.length - 1) { | ||
} | ||
// This is relevant only for the Backend JS Console | ||
toOriginalExpression() { | ||
var args = []; | ||
var s = '('; | ||
for (var i = 0; i < this.operands.length; i++) { | ||
var operand = this.operands[i]; | ||
if (operand instanceof TrueExpression) { | ||
let args = []; | ||
let s = '('; | ||
for (let i = 0; i < this.operands.length; i++) { | ||
const operand = this.operands[i]; | ||
if (operand instanceof TrueExpression) | ||
continue; | ||
let opPair = operand.toOriginalExpression(); | ||
if (opPair[0] == '') { | ||
// nested TrueExpression | ||
continue; | ||
} | ||
var opPair = operand.toOriginalExpression(); | ||
s += opPair[0]; | ||
args.push(_tail(opPair)); | ||
args = _flatten(args); | ||
if (i != this.operands.length - 1) { | ||
if (s.length > 1) { | ||
// Not the first operand | ||
s += ' and '; | ||
} | ||
s += opPair[0]; | ||
args.push(_.tail(opPair)); | ||
args = _.flatten(args); | ||
} | ||
s += ')'; | ||
return _flatten([s, args]); | ||
if (s.length == 2) { | ||
return new TrueExpression().toOriginalExpression(); | ||
} | ||
return _.flatten([s, args]); | ||
} | ||
@@ -369,3 +302,3 @@ } | ||
normalize() { | ||
var newOperands = []; | ||
const newOperands = []; | ||
this.operands.forEach(function (op) { | ||
@@ -380,4 +313,4 @@ const norm = op.normalize(); | ||
evaluate(object) { | ||
for (var i = 0; i < this.operands.length; i++) { | ||
var operand = this.operands[i]; | ||
for (let i = 0; i < this.operands.length; i++) { | ||
const operand = this.operands[i]; | ||
if (operand.evaluate(object)) { | ||
@@ -389,14 +322,6 @@ return true; | ||
} | ||
toJSON() { | ||
return { | ||
__type: 'OrExpression', | ||
operands: this.operands.map(function (o) { | ||
return o.toJSON(); | ||
}) | ||
}; | ||
} | ||
toString() { | ||
var s = '('; | ||
for (var i = 0; i < this.operands.length; i++) { | ||
var operand = this.operands[i]; | ||
let s = '('; | ||
for (let i = 0; i < this.operands.length; i++) { | ||
const operand = this.operands[i]; | ||
s += operand.toString(); | ||
@@ -410,15 +335,15 @@ if (i != this.operands.length - 1) { | ||
} | ||
// This is relevant only for the Backend JS Console | ||
toOriginalExpression() { | ||
var args = []; | ||
var s = '('; | ||
for (var i = 0; i < this.operands.length; i++) { | ||
var operand = this.operands[i]; | ||
let args = []; | ||
let s = '('; | ||
for (let i = 0; i < this.operands.length; i++) { | ||
const operand = this.operands[i]; | ||
if (operand instanceof TrueExpression) { | ||
continue; | ||
// One TrueExpression makes the entire OR clause true | ||
return operand.toOriginalExpression(); | ||
} | ||
var opPair = operand.toOriginalExpression(); | ||
const opPair = operand.toOriginalExpression(); | ||
s += opPair[0]; | ||
args.push(_tail(opPair)); | ||
args = _flatten(args); | ||
args.push(_.tail(opPair)); | ||
args = _.flatten(args); | ||
if (i != this.operands.length - 1) { | ||
@@ -429,3 +354,3 @@ s += ' or '; | ||
s += ')'; | ||
return _flatten([s, args]); | ||
return _.flatten([s, args]); | ||
} | ||
@@ -437,2 +362,3 @@ } | ||
class TrueExpression { | ||
constructor() { } | ||
normalize() { | ||
@@ -448,11 +374,13 @@ return new OrExpression([new AndExpression([this])]); | ||
toOriginalExpression() { | ||
return []; | ||
return ['']; | ||
} | ||
toJSON() { | ||
return { | ||
__type: 'TrueExpression' | ||
}; | ||
} | ||
} | ||
exports.TrueExpression = TrueExpression; | ||
// We should probably support international characters as well | ||
function isLetter(character) { | ||
return (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z'); | ||
} | ||
function isDigit(character) { | ||
return character >= '0' && character <= '9'; | ||
} | ||
const OPERATORS = ['>=', '<=', '=', '!=', '>', '<', 'starts with', 'contains']; | ||
@@ -465,14 +393,12 @@ const BOOLEAN_LOGIC = ['and', 'or']; | ||
constructor(expression) { | ||
this.expression = expression; | ||
this.i = 0; | ||
this.interpolatorCount = 0; | ||
this.expression = expression; | ||
if (expression == null) { | ||
throw new Error('expression is required'); | ||
} | ||
this.advance(); | ||
} | ||
exception(message, start, end) { | ||
var error = new Error(message + | ||
'\n' + | ||
this.expression + | ||
'\n' + | ||
new Array(this.i + 1).join(' ') + | ||
'^'); | ||
let error = new Error(message + '\n' + this.expression + '\n' + new Array(this.i + 1).join(' ') + '^'); | ||
if (start == null) { | ||
@@ -486,5 +412,8 @@ start = this.i; | ||
} | ||
isWordSeparator(p) { | ||
return p >= this.expression.length || this.expression[p] == ' '; | ||
} | ||
isWord() { | ||
this.checkBuffer(); | ||
return (this.isLetter(this.expression[this.i]) || this.expression[this.i] == '_'); | ||
return isLetter(this.expression[this.i]) || this.expression[this.i] == '_'; | ||
} | ||
@@ -520,3 +449,3 @@ hasMore() { | ||
} | ||
var c = this.expression[this.i]; | ||
const c = this.expression[this.i]; | ||
this.i++; | ||
@@ -526,26 +455,6 @@ this.advance(); | ||
} | ||
// Read a single alphanumeric word. May contain underscores. | ||
readWord() { | ||
this.checkBuffer(); | ||
var word = ''; | ||
word += this.expression[this.i]; | ||
var offset = 1; | ||
while (this.expression.length > this.i + offset) { | ||
const char = this.expression[this.i + offset]; | ||
if (this.isLetter(char) || this.isDigit(char) || char == '_') { | ||
word += this.expression[this.i + offset++]; | ||
} | ||
else { | ||
break; | ||
} | ||
} | ||
this.i += offset; | ||
this.advance(); | ||
return word; | ||
} | ||
// Read an interpolation character "?" | ||
readInterpolator() { | ||
this.checkBuffer(); | ||
if (this.expression[this.i] == '?' && | ||
(this.isWordSeparator(this.i + 1) || this.expression[this.i + 1] == ')')) { | ||
if (this.expression[this.i] == '?' && (this.isWordSeparator(this.i + 1) || this.expression[this.i + 1] == ')')) { | ||
this.i += 1; | ||
@@ -559,2 +468,18 @@ this.advance(); | ||
} | ||
// Read a single alphanumeric word. May contain underscores. | ||
readWord() { | ||
this.checkBuffer(); | ||
let word = ''; | ||
word += this.expression[this.i]; | ||
let offset = 1; | ||
while (this.expression.length > this.i + offset && | ||
(isLetter(this.expression[this.i + offset]) || | ||
isDigit(this.expression[this.i + offset]) || | ||
this.expression[this.i + offset] == '_')) { | ||
word += this.expression[this.i + offset++]; | ||
} | ||
this.i += offset; | ||
this.advance(); | ||
return word; | ||
} | ||
// Advance to the next non-whitespace character. | ||
@@ -567,13 +492,2 @@ advance() { | ||
} | ||
isWordSeparator(p) { | ||
return p >= this.expression.length || this.expression[p] == ' '; | ||
} | ||
// We should probably support international characters as well | ||
isLetter(character) { | ||
return ((character >= 'a' && character <= 'z') || | ||
(character >= 'A' && character <= 'Z')); | ||
} | ||
isDigit(character) { | ||
return character >= '0' && character <= '9'; | ||
} | ||
checkBuffer() { | ||
@@ -589,4 +503,4 @@ if (!this.hasMore()) { | ||
this.checkBuffer(); | ||
for (var j = 0; j < tokens.length; j++) { | ||
var token = tokens[j]; | ||
for (let j = 0; j < tokens.length; j++) { | ||
const token = tokens[j]; | ||
if (this.i + token.length <= this.expression.length) { | ||
@@ -605,8 +519,7 @@ if (token == this.expression.substring(this.i, this.i + token.length)) { | ||
this.checkBuffer(); | ||
for (var j = 0; j < tokens.length; j++) { | ||
var token = tokens[j]; | ||
for (let j = 0; j < tokens.length; j++) { | ||
const token = tokens[j]; | ||
if (this.i + token.length <= this.expression.length) { | ||
if (token == this.expression.substring(this.i, this.i + token.length)) { | ||
if (!this.isLetter(token[token.length - 1]) || | ||
this.isWordSeparator(this.i + token.length)) { | ||
if (!isLetter(token[token.length - 1]) || this.isWordSeparator(this.i + token.length)) { | ||
return true; | ||
@@ -625,13 +538,13 @@ } | ||
constructor(scopeType, tokenizer, args) { | ||
this.scopeType = scopeType; | ||
this.tokenizer = tokenizer; | ||
this.args = args; | ||
if (args == null) { | ||
args = []; | ||
this.args = []; | ||
} | ||
this.args = args; | ||
this.scopeType = scopeType; | ||
this.tokenizer = tokenizer; | ||
} | ||
parse() { | ||
var e = this.parseOrExpression(); | ||
const e = this.parseOrExpression(); | ||
if (this.tokenizer.hasMore()) { | ||
throw this.tokenizer.exception('Unexpected token'); | ||
throw this.tokenizer.exception('Invalid expression (expected expression to end)'); | ||
} | ||
@@ -641,10 +554,10 @@ return e; | ||
/* | ||
expr : term ( ( AND | OR ) term ) * | ||
term : ( attribute operator value ) | ( \( expression \) ) | ||
factor : NUMBER ; | ||
expr : term ( ( AND | OR ) term ) * | ||
term : ( attribute operator value ) | ( \( expression \) ) | ||
factor : NUMBER ; | ||
*/ | ||
parseOrExpression() { | ||
var e = this.parseAndExpression(); | ||
const e = this.parseAndExpression(); | ||
if (this.tokenizer.hasMore() && this.tokenizer.isOr()) { | ||
var expressions = [e]; | ||
let expressions = [e]; | ||
while (this.tokenizer.hasMore() && this.tokenizer.isOr()) { | ||
@@ -661,5 +574,5 @@ this.tokenizer.readBooleanLogic(); | ||
parseAndExpression() { | ||
var e = this.parseTerm(); | ||
const e = this.parseTerm(); | ||
if (this.tokenizer.hasMore() && this.tokenizer.isAnd()) { | ||
var expressions = [e]; | ||
let expressions = [e]; | ||
while (this.tokenizer.hasMore() && this.tokenizer.isAnd()) { | ||
@@ -678,14 +591,10 @@ this.tokenizer.readBooleanLogic(); | ||
let attributeStart = this.tokenizer.i; | ||
var attributeName = this.tokenizer.readWord(); | ||
const attributeName = this.tokenizer.readWord(); | ||
let attributeEnd = this.tokenizer.previousPosition + 1; | ||
var operator = this.tokenizer.readOperator(); | ||
const operator = this.tokenizer.readOperator(); | ||
let interpolatorPosition = this.tokenizer.i; | ||
var valueIndex = this.tokenizer.readInterpolator(); | ||
var attribute = this.scopeType.getAttribute(attributeName); | ||
const valueIndex = this.tokenizer.readInterpolator(); | ||
const attribute = this.scopeType.getAttribute(attributeName); | ||
if (attribute == null) { | ||
throw this.tokenizer.exception("'" + | ||
attributeName + | ||
"' is not a valid field of '" + | ||
this.scopeType.name + | ||
"'", attributeStart, attributeEnd); | ||
throw this.tokenizer.exception("'" + attributeName + "' is not a valid field of '" + this.scopeType.name + "'", attributeStart, attributeEnd); | ||
} | ||
@@ -696,3 +605,3 @@ else if (attribute.type.isCollection) { | ||
if (this.args.length > valueIndex) { | ||
var value = this.args[valueIndex]; | ||
let value = this.args[valueIndex]; | ||
if (attribute.type.isObject && value != null) { | ||
@@ -708,7 +617,7 @@ value = value.id; | ||
else if (this.tokenizer.isParenthesis()) { | ||
var c = this.tokenizer.readParenthesis(); | ||
let c = this.tokenizer.readParenthesis(); | ||
if (c != '(') { | ||
throw this.tokenizer.exception('Expected (', this.tokenizer.previousPosition); | ||
} | ||
var e = this.parseOrExpression(); | ||
const e = this.parseOrExpression(); | ||
c = this.tokenizer.readParenthesis(); | ||
@@ -715,0 +624,0 @@ if (c != ')') { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const IndexedAdapter_1 = require("./IndexedAdapter"); | ||
const BaseAdapter_1 = require("./BaseAdapter"); | ||
@@ -14,5 +13,5 @@ var logQuery = BaseAdapter_1.BaseAdapter.logQuery; | ||
} | ||
class WebSQLAdapter extends IndexedAdapter_1.IndexedAdapter { | ||
class WebSQLAdapter extends BaseAdapter_1.BaseAdapter { | ||
constructor(options) { | ||
super(options); | ||
super(); | ||
this.db = null; | ||
@@ -52,18 +51,10 @@ if (options.stf) { | ||
} | ||
if (version < 4) { | ||
if (version < 6) { | ||
tx.loggedSql('DROP TABLE IF EXISTS object_indexes', []); | ||
tx.loggedSql('CREATE TABLE IF NOT EXISTS object_indexes (id TEXT, type TEXT, index_name TEXT, key BLOB, index_version)', []); | ||
tx.loggedSql('CREATE INDEX IF NOT EXISTS indexes_by_object ON object_indexes(type, id)', []); | ||
tx.loggedSql('CREATE INDEX IF NOT EXISTS indexes_by_key ON object_indexes(type, index_name, key)', []); | ||
tx.loggedSql('ALTER TABLE objects ADD COLUMN index_version INTEGER', []); | ||
tx.loggedSql('CREATE INDEX IF NOT EXISTS objects_by_index_version ON objects(type, index_version)', []); | ||
tx.loggedSql('DROP INDEX IF EXISTS objects_by_index_version', []); | ||
} | ||
if (version < 5) { | ||
tx.loggedSql('CREATE INDEX IF NOT EXISTS indexes_by_type_version ON object_indexes(type, index_version)', []); | ||
} | ||
version = 5; | ||
version = 6; | ||
tx.loggedSql('INSERT OR REPLACE INTO versions(name, version) VALUES(?, ?)', ['db', version]); | ||
}); | ||
}) | ||
.then(this.loadIndexes.bind(this)); | ||
}); | ||
} | ||
@@ -102,32 +93,2 @@ /** | ||
} | ||
/** | ||
* Update the indexes to the specified set. | ||
* This runs in an exclusive lock, blocking all other database writes and all queries. | ||
* This operation is idempotent, and should be fast if all the indexes | ||
* are already up to date. | ||
*/ | ||
updateIndexes(indexes) { | ||
var self = this; | ||
var totalCount = 0; | ||
return this.indexQueue.enqueue(function () { | ||
var promises = []; | ||
indexes.modelIndexes().forEach(function (modelIndexes) { | ||
// modelIndexes is the index per model. | ||
// TODO: it may be better to do these updates sequentially. | ||
var promise = self._updateIndexData(modelIndexes).then(function (count) { | ||
totalCount += count; | ||
}); | ||
promises.push(promise); | ||
}); | ||
return Promise.all(promises).then(function () { | ||
// Update the current version of index (for queries and writes). | ||
// Do this inside the indexQueue block, so that any queries | ||
// waiting on us will use the new indexes. | ||
self.indexes = indexes; | ||
// The count is useful mostly for specs, so that we can confirm that the correct number | ||
// of objects were updated. | ||
return { updated: totalCount }; | ||
}); | ||
}); | ||
} | ||
applyCrud(crud) { | ||
@@ -142,11 +103,7 @@ this.ensureOpen(); | ||
if (action == 'put') { | ||
var syncInfo = { | ||
hash: data.hash, | ||
update_id: data.update_id | ||
}; | ||
ids.push(data.id); | ||
this.transactionSaveData(tx, data, null, syncInfo); | ||
this.transactionSaveData(tx, data, null); | ||
} | ||
else if (action == 'delete') { | ||
this.transactionDestroy(tx, data.type, data.id, true); | ||
this.transactionDestroy(tx, data.type, data.id); | ||
} | ||
@@ -167,6 +124,6 @@ else { | ||
*/ | ||
applyBatch(crud) { | ||
async applyBatch(crud) { | ||
this.ensureOpen(); | ||
var results = []; | ||
return this.promisedTransaction((tx) => { | ||
await this.promisedTransaction((tx) => { | ||
for (var i = 0; i < crud.length; i++) { | ||
@@ -189,17 +146,23 @@ var operation = crud[i]; | ||
} | ||
}) | ||
.then(() => { | ||
return this.dataChanged(); | ||
}) | ||
.then(() => { | ||
return results; | ||
}); | ||
return results; | ||
} | ||
async save(data, patch) { | ||
this.ensureOpen(); | ||
if (patch != null) { | ||
if (Object.keys(patch.attributes).length === 0 && | ||
Object.keys(patch.belongs_to).length === 0) { | ||
// No change - don't save anything | ||
return null; | ||
} | ||
} | ||
await this.saveData(data, patch); | ||
} | ||
// Persist an object and its relationships. | ||
// The object and it's relationships MUST be loaded before this is called. | ||
// Returns a promise that is resolved when the object is persisted. | ||
saveData(data, patch, syncInfo) { | ||
saveData(data, patch) { | ||
this.ensureOpen(); | ||
return this.promisedTransaction(tx => { | ||
this.transactionSaveData(tx, data, patch, syncInfo); | ||
this.transactionSaveData(tx, data, patch); | ||
}); | ||
@@ -212,11 +175,34 @@ } | ||
*/ | ||
destroy(type, id, remoteChange) { | ||
async destroy(type, id) { | ||
this.ensureOpen(); | ||
var self = this; | ||
return this.promisedTransaction(function (tx) { | ||
self.transactionDestroy(tx, type, id, remoteChange); | ||
}).then(function () { | ||
IndexedAdapter_1.IndexedAdapter.prototype.destroy.call(self, type, id, remoteChange); | ||
await this.promisedTransaction(async (tx) => { | ||
await this.transactionDestroy(tx, type, id); | ||
}); | ||
await super.destroy(type, id); | ||
} | ||
/** | ||
* Perform a query. | ||
* Returns a promise that is resolved with the resultset. | ||
*/ | ||
async executeQuery(query) { | ||
const skip = query.skipNumber; | ||
const limit = query.limitNumber; | ||
const { data } = await this.executeTableScan(query); | ||
let result = []; | ||
if (skip >= data.length) { | ||
return result; | ||
} | ||
const startIndex = !skip ? 0 : skip; | ||
for (let i = startIndex, recordCount = 0; i < data.length && (limit == null || recordCount < limit); i++, recordCount++) { | ||
result.push(data[i]); | ||
} | ||
return result; | ||
} | ||
explain(query) { | ||
const start = new Date(); | ||
return this.executeTableScan(query).then(function (results) { | ||
results.duration = new Date().getTime() - start.getTime(); | ||
return results; | ||
}); | ||
} | ||
executeTableScan(query) { | ||
@@ -311,191 +297,2 @@ this.ensureOpen(); | ||
} | ||
getByIndex(range) { | ||
var key = range.toSqlRange('object_indexes.key'); | ||
var query = 'SELECT objects.* FROM objects INNER JOIN object_indexes ON objects.type = object_indexes.type AND objects.id = object_indexes.id' + | ||
' WHERE object_indexes.type=? AND object_indexes.index_name=? AND ' + | ||
key.clause; | ||
return this.simpleQuery(query, [ | ||
key.type, | ||
key.index, | ||
key.lower, | ||
key.upper | ||
]).then(function (rs) { | ||
var all = []; | ||
for (var i = 0; i < rs.rows.length; i++) { | ||
var row = rs.rows.item(i); | ||
var data = JSON.parse(row.data); | ||
data.update_id = row.uid; | ||
data.hash = row.hash; | ||
all.push(data); | ||
} | ||
return all; | ||
}); | ||
} | ||
getByIndexes(typeName, ranges) { | ||
var self = this; | ||
var all = {}; | ||
var promises = []; | ||
ranges.forEach(function (range) { | ||
promises.push(self.getByIndex(range).then(function (results) { | ||
results.forEach(function (object) { | ||
all[object.id] = object; | ||
}); | ||
})); | ||
}); | ||
return Promise.all(promises).then(function () { | ||
var objects = []; | ||
for (var id in all) { | ||
objects.push(all[id]); | ||
} | ||
return objects; | ||
}); | ||
} | ||
/** | ||
* Get a batch of objects to send to the server. | ||
* When the objects are successfully sent to the server, call crudComplete(t). | ||
* Returns a promise resolving with: {crud: [], transaction: t} | ||
*/ | ||
getCrudBatch(limit) { | ||
this.ensureOpen(); | ||
limit = limit || 100; | ||
return this.simpleQuery('SELECT * FROM crud ORDER BY id ASC LIMIT ?', [ | ||
limit | ||
]).then(function (rs) { | ||
var all = []; | ||
var transactionDetails = []; | ||
for (var i = 0; i < rs.rows.length; i++) { | ||
var row = rs.rows.item(i); | ||
var data = JSON.parse(row.data); | ||
var id = row.id; | ||
all.push(data); | ||
transactionDetails.push(id); | ||
} | ||
if (all.length === 0) { | ||
return null; | ||
} | ||
else { | ||
return { crud: all, transaction: transactionDetails }; | ||
} | ||
}); | ||
} | ||
crudComplete(transaction) { | ||
this.ensureOpen(); | ||
// Ugly, but faster than a separate query for every CRUD message (unconfirmed) | ||
var q = queryIn(transaction.length); | ||
return this.simpleQuery('DELETE FROM crud WHERE id IN (' + q + ')', transaction).then(() => null); | ||
} | ||
/** | ||
* Delete all objects in the specified range, excluding those listed in the `skip` array (optional). | ||
*/ | ||
clearRange(start, end, skip) { | ||
// There is a limit to the number of variables allowed | ||
this.ensureOpen(); | ||
skip = skip || []; | ||
var q = queryIn(skip.length); | ||
start = start || ''; | ||
return this.simpleQuery('DELETE FROM objects WHERE uid >= ? AND uid < ? AND id NOT IN (' + | ||
q + | ||
')', [start, end].concat(skip)).then(() => null); | ||
} | ||
// Clear all objects without an uid. | ||
// We ONLY do this if the crud table is empty. Otherwise, we may get issues where the local data is cleared | ||
// before it was even sent to the server. | ||
clearLocal() { | ||
this.ensureOpen(); | ||
var cleared; | ||
var error = null; | ||
// It is important that this happens in the same transaction, so that it doesn't overlap with a saveData() call. | ||
return this.promisedTransaction(tx => { | ||
tx.loggedSql('SELECT COUNT(*) as count FROM crud', [], function (tx2, rs) { | ||
try { | ||
var count = 0; | ||
if (rs.rows.length >= 1) { | ||
var row = rs.rows.item(0); | ||
count = row.count; | ||
} | ||
else { | ||
// Should not happen. Assume count = 0. | ||
} | ||
if (count === 0) { | ||
cleared = true; | ||
tx2.loggedSql('DELETE FROM objects WHERE uid IS NULL', []); | ||
} | ||
else { | ||
cleared = false; | ||
} | ||
} | ||
catch (err) { | ||
this.logError('Error during clearLocal() callback', err); | ||
error = err; | ||
} | ||
}); | ||
}).then(() => { | ||
if (error != null) { | ||
throw error; | ||
} | ||
else { | ||
return cleared; | ||
} | ||
}); | ||
} | ||
/** | ||
* Sum the hashes of all objects in the specified range. | ||
* */ | ||
calculateHash(start, end) { | ||
this.ensureOpen(); | ||
start = start || ''; | ||
return this.simpleQuery('SELECT SUM(hash) AS hash, COUNT(id) AS count FROM objects WHERE uid >= ? AND uid < ?', [start, end]).then(function (rs) { | ||
if (rs.rows.length >= 1) { | ||
var row = rs.rows.item(0); | ||
// row.hash is null if there are nothing in the specified range. | ||
var hash = row.hash || 0; | ||
// Hash should be 32 bits only | ||
hash = hash & 0xffffffff; | ||
if (hash >= 0x80000000) { | ||
hash -= 0x100000000; | ||
} | ||
return { hash: hash, count: row.count }; | ||
} | ||
else { | ||
// Should not happen | ||
return { hash: 0, count: 0 }; | ||
} | ||
}); | ||
} | ||
// Return the highest update id in the database, as a promise. | ||
getLastUpdateId() { | ||
this.ensureOpen(); | ||
return this.simpleQuery('SELECT MAX(uid) AS uid FROM objects', []).then(function (rs) { | ||
if (rs.rows.length >= 1) { | ||
var row = rs.rows.item(0); | ||
return row.uid; | ||
} | ||
else { | ||
// Empty table | ||
return null; | ||
} | ||
}); | ||
} | ||
// Private function, but exported for tests | ||
uidAtOffset(start, end, offset) { | ||
this.ensureOpen(); | ||
return this.simpleQuery('SELECT uid FROM objects where uid >= ? AND uid < ? ORDER BY uid LIMIT 1 offset ?', [start, end, offset]).then(function (rs) { | ||
if (rs.rows.length >= 1) { | ||
var row = rs.rows.item(0); | ||
return row.uid; | ||
} | ||
else { | ||
return null; | ||
} | ||
}); | ||
} | ||
uidAtOffsets(start, end, offsets) { | ||
this.ensureOpen(); | ||
// TODO: wrap this in a single transaction | ||
var promises = []; | ||
for (var i = 0; i < offsets.length; i++) { | ||
promises.push(this.uidAtOffset(start, end, offsets[i])); | ||
} | ||
return Promise.all(promises); | ||
} | ||
estimateBytesUsed() { | ||
@@ -532,32 +329,2 @@ // We only count the length of the dynamically-sized data here | ||
} | ||
estimateQueueBytesUsed() { | ||
// We only count the length of the dynamically-sized data here | ||
var dataSum = this.simpleQuery('SELECT SUM(LENGTH(data)) as sum FROM crud').then(function (rs) { | ||
if (rs.rows.length >= 1) { | ||
var row = rs.rows.item(0); | ||
return row.sum; | ||
} | ||
else { | ||
return 0; | ||
} | ||
}); | ||
var objectCount = this.simpleQuery('SELECT COUNT(id) as count FROM crud').then(function (rs) { | ||
if (rs.rows.length >= 1) { | ||
var row = rs.rows.item(0); | ||
return row.count; | ||
} | ||
else { | ||
return 0; | ||
} | ||
}); | ||
return Promise.all([dataSum, objectCount]).then(function (results) { | ||
var sum = results[0]; | ||
var count = results[1]; | ||
var stringCharacters = sum; | ||
// 20 bytes overhead per record (including the ID) | ||
var overhead = 20 * count; | ||
// 2 bytes per character | ||
return stringCharacters * 2 + overhead; | ||
}); | ||
} | ||
openDatabase(name) { | ||
@@ -610,136 +377,12 @@ return window.openDatabase(name, 'Objects', 20 * 1024 * 1024, function () { | ||
} | ||
transactionSaveData(tx, data, patch, syncInfo) { | ||
var updateId = null; | ||
var hash = null; | ||
// Save to crud queue, only if: | ||
// 1. Not downloaded through sync service (sync_info == null) | ||
// 2. This is not a local-only database (self.sync) | ||
if (syncInfo == null) { | ||
if (this.sync) { | ||
if (patch != null) { | ||
tx.loggedSql('INSERT INTO crud(data) VALUES (?)', [ | ||
JSON.stringify({ patch: patch }) | ||
]); | ||
} | ||
else { | ||
tx.loggedSql('INSERT INTO crud(data) VALUES (?)', [ | ||
JSON.stringify({ put: data }) | ||
]); | ||
} | ||
} | ||
} | ||
else { | ||
updateId = syncInfo.update_id; | ||
hash = syncInfo.hash; | ||
} | ||
var modelIndexes = this.indexes.indexesFor(data.type); | ||
tx.loggedSql('INSERT OR REPLACE INTO objects(id, type, data, uid, hash, index_version) VALUES (?,?,?,?,?,?)', [ | ||
transactionSaveData(tx, data, patch) { | ||
tx.loggedSql('INSERT OR REPLACE INTO objects(id, type, data) VALUES (?,?,?)', [ | ||
data.id, | ||
data.type, | ||
JSON.stringify(data), | ||
updateId, | ||
hash, | ||
modelIndexes.version | ||
JSON.stringify(data) | ||
]); | ||
// Update indexes | ||
this._updateItemIndexes(tx, modelIndexes, data); | ||
} | ||
transactionDestroy(tx, type, id, remoteChange) { | ||
if (!remoteChange) { | ||
tx.loggedSql('INSERT INTO crud(data) VALUES (?)', [ | ||
JSON.stringify({ delete: { id: id, type: type } }) | ||
]); | ||
} | ||
transactionDestroy(tx, type, id) { | ||
tx.loggedSql('DELETE FROM objects WHERE type=? AND id=?', [type, id]); | ||
// Update indexes | ||
tx.loggedSql('DELETE FROM object_indexes WHERE id=? AND type=?', [ | ||
id, | ||
type | ||
]); | ||
} | ||
_updateItemIndexes(transaction, modelIndexes, data, del) { | ||
if (del != 'NO_DELETE') { | ||
transaction.loggedSql('DELETE FROM object_indexes WHERE id=? AND type=?', [data.id, data.type]); | ||
} | ||
var indexKeys = modelIndexes.generateSqlKeys(data); | ||
Object.keys(indexKeys).forEach(function (indexName) { | ||
var key = indexKeys[indexName]; | ||
transaction.loggedSql('INSERT INTO object_indexes(id, type, index_name, key, index_version) VALUES (?,?,?,?,?)', [data.id, data.type, indexName, key, modelIndexes.version]); | ||
}); | ||
} | ||
// Update the index data for a model. | ||
_updateIndexData(modelIndexes) { | ||
var self = this; | ||
var typeName = modelIndexes.type; | ||
var version = modelIndexes.version; | ||
// We split this into batches of 1000, each batch inside a transaction. | ||
// Larger transactions use too much memory, and small transactions have | ||
// too much transaction overhead. | ||
var BATCH_SIZE = 1000; | ||
function processBatch() { | ||
var count; | ||
return self | ||
.promisedTransaction(function (transaction) { | ||
return new Promise(function (resolve, reject) { | ||
function processResults(tx, rs) { | ||
var n = rs.rows.length; | ||
for (var i = 0; i < n; i++) { | ||
var row = rs.rows.item(i); | ||
var data = JSON.parse(row.data); | ||
// Update the indexes for this item. | ||
self._updateItemIndexes(transaction, modelIndexes, data, 'NO_DELETE'); | ||
transaction.loggedSql('UPDATE objects SET index_version = ? WHERE type = ? and id = ?', [version, typeName, row.id]); | ||
} | ||
count = n; | ||
resolve(); | ||
} | ||
// Find a batch of objects of this model with outdated indexes. | ||
// We want a query for: | ||
// SELECT * FROM objects WHERE objects.type = ? AND objects.index_version != ? | ||
// However, that doesn't use index_version in the index. | ||
// (index_version < ? OR index_version > ?) seems to work for select queries. | ||
transaction.loggedSql('SELECT * FROM objects WHERE objects.type = ? AND (objects.index_version < ? OR objects.index_version > ?) LIMIT ?', [ | ||
typeName, | ||
modelIndexes.version, | ||
modelIndexes.version, | ||
BATCH_SIZE | ||
], processResults); | ||
}); | ||
}) | ||
.then(function () { | ||
return count; | ||
}); | ||
} | ||
function recursiveBatches() { | ||
return processBatch().then(function (count) { | ||
if (count > 0) { | ||
// We processed data, and there may be more to process. | ||
return recursiveBatches().then(function (nextCount) { | ||
return count + nextCount; | ||
}); | ||
} | ||
else { | ||
// We are done here | ||
return count; | ||
} | ||
}); | ||
} | ||
return recursiveBatches().then(count => { | ||
// After processing all the data, delete the old indexes. | ||
// We only delete the index afterwards, so that we're not in a | ||
// completely inconsistent state if the rebuild is interrupted. | ||
return this.promisedTransaction(function (transaction) { | ||
// We want a query for: | ||
// DELETE FROM object_indexes WHERE type = ? and index_version != ? | ||
// However, that doesn't use index_version in the index. | ||
// (index_version < ? OR index_version > ?) uses the index for SELECT, but not DELETE. | ||
// Doing this in two separate queries seems to be the best solution for now. | ||
// TODO: if we guaruntee that index_version is always increasing, we can remove the > query. | ||
transaction.loggedSql('DELETE FROM object_indexes WHERE type = ? and index_version < ?', [typeName, version]); | ||
transaction.loggedSql('DELETE FROM object_indexes WHERE type = ? and index_version > ?', [typeName, version]); | ||
}).then(function () { | ||
return count; | ||
}); | ||
}); | ||
} | ||
logError(...args) { | ||
@@ -746,0 +389,0 @@ console.error(...args); // tslint:disable-line |
{ | ||
"name": "@journeyapps/db", | ||
"version": "0.0.0-dev.362799e.d299e33", | ||
"version": "0.0.0-dev.375c2d3.0ce5033", | ||
"description": "Journey JS library", | ||
"main": "./dist/src/index.js", | ||
"browser": "./dist/src/browser.js", | ||
"typings": "./dist/@types/src/index", | ||
@@ -14,6 +15,6 @@ "scripts": { | ||
"dependencies": { | ||
"@journeyapps/core-date": "0.0.0-dev.362799e.d299e33", | ||
"@journeyapps/core-xml": "0.0.0-dev.362799e.d299e33", | ||
"@journeyapps/evaluator": "0.0.0-dev.362799e.d299e33", | ||
"@journeyapps/parser-schema": "0.0.0-dev.362799e.d299e33", | ||
"@journeyapps/core-date": "0.0.0-dev.375c2d3.0ce5033", | ||
"@journeyapps/core-xml": "0.0.0-dev.375c2d3.0ce5033", | ||
"@journeyapps/evaluator": "0.0.0-dev.375c2d3.0ce5033", | ||
"@journeyapps/parser-schema": "0.0.0-dev.375c2d3.0ce5033", | ||
"isomorphic-fetch": "^2.2.1", | ||
@@ -31,3 +32,3 @@ "moment": "^2.24.0", | ||
], | ||
"gitHead": "fa37b6feabcb4c1474d813d54d8380c4d5137029" | ||
"gitHead": "9eabd513e30ef59a7c471e2999bf2189201bb6da" | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
3
288291
90
4553
+ Added@journeyapps/core-date@0.0.0-dev.375c2d3.0ce5033(transitive)
+ Added@journeyapps/core-xml@0.0.0-dev.375c2d3.0ce5033(transitive)
+ Added@journeyapps/evaluator@0.0.0-dev.375c2d3.0ce5033(transitive)
+ Added@journeyapps/parser-common@0.0.0-dev.375c2d3.0ce5033(transitive)
+ Added@journeyapps/parser-schema@0.0.0-dev.375c2d3.0ce5033(transitive)
- Removed@journeyapps/core-date@0.0.0-dev.362799e.d299e33(transitive)
- Removed@journeyapps/core-xml@0.0.0-dev.362799e.d299e33(transitive)
- Removed@journeyapps/evaluator@0.0.0-dev.362799e.d299e33(transitive)
- Removed@journeyapps/parser-common@0.0.0-dev.362799e.d299e33(transitive)
- Removed@journeyapps/parser-schema@0.0.0-dev.362799e.d299e33(transitive)