diff --git a/.gitignore b/.gitignore index 9af9dbc..2007561 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ tmp bin build *.zip -.DS_Store -.metadata +.* diff --git a/.gitmodules b/.gitmodules index 53f6efd..1179dd0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "mobile/ios/vendor/couchbase-lite-ios"] path = mobile/ios/vendor/couchbase-lite-ios url = https://github.com/couchbase/couchbase-lite-ios.git - branch = release/1.0.0 + branch = release/1.0.2 diff --git a/mobile/android/.gitignore b/mobile/android/.gitignore index f8715a9..8adbbe7 100644 --- a/mobile/android/.gitignore +++ b/mobile/android/.gitignore @@ -7,3 +7,5 @@ build build.properties dist libs +example +documentation diff --git a/mobile/android/build.xml b/mobile/android/build.xml index a78421b..ab645e4 100644 --- a/mobile/android/build.xml +++ b/mobile/android/build.xml @@ -9,7 +9,6 @@ - diff --git a/mobile/android/example b/mobile/android/example deleted file mode 120000 index f307990..0000000 --- a/mobile/android/example +++ /dev/null @@ -1 +0,0 @@ -../noarch/example \ No newline at end of file diff --git a/mobile/android/lib/LICENSE.txt b/mobile/android/lib/LICENSE.txt new file mode 100644 index 0000000..bdf0aa1 --- /dev/null +++ b/mobile/android/lib/LICENSE.txt @@ -0,0 +1,17 @@ +Couchbase, Inc. Community Edition License Agreement + +IMPORTANT-READ CAREFULLY: BY CLICKING THE “I ACCEPT” BOX OR INSTALLING, DOWNLOADING OR OTHERWISE USING THIS SOFTWARE AND ANY ASSOCIATED DOCUMENTATION, YOU, ON BEHALF OF YOURSELF OR AS AN AUTHORIZED REPRESENTATIVE ON BEHALF OF AN ENTITY (“LICENSEE”) AGREE TO ALL THE TERMS OF THIS COMMUNITY EDITION LICENSE AGREEMENT (THE “AGREEMENT”) REGARDING YOUR USE OF THE SOFTWARE. YOU REPRESENT AND WARRANT THAT YOU HAVE FULL LEGAL AUTHORITY TO BIND THE LICENSEE TO THIS AGREEMENT. IF YOU DO NOT AGREE WITH ALL OF THESE TERMS, DO NOT SELECT THE “I ACCEPT” BOX AND DO NOT INSTALL, DOWNLOAD OR OTHERWISE USE THE SOFTWARE. THE EFFECTIVE DATE OF THIS AGREEMENT IS THE DATE ON WHICH YOU CLICK “I ACCEPT” OR OTHERWISE INSTALL, DOWNLOAD OR USE THE SOFTWARE. + +1. License Grant. Couchbase Inc. hereby grants Licensee, free of charge, the non-exclusive right to use, copy, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to Licensee including the following copyright notice in all copies or substantial portions of the Software: + +Couchbase ® +http://www.couchbase.com +Copyright 2011 Couchbase, Inc. + +As used in this Agreement, “Software” means the object code version of the applicable elastic data management server software provided by Couchbase, Inc. + +2. Restrictions. Licensee will not: (a) reverse engineer, disassemble, or decompile the Software (except to the extent such restrictions are prohibited by law); + +3. Support. Couchbase, Inc. will provide Licensee with access to, and use of, the Couchbase, Inc. support forum available at the following URL: http://forums.Couchbase.org. Couchbase, Inc. may, at its discretion, modify, suspend or terminate support at any time. + +4. Warranty Disclaimer and Limitation of Liability. THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL COUCHBASE INC. OR THE AUTHORS OR COPYRIGHT HOLDERS IN THE SOFTWARE BE LIABLE FOR ANY CLAIM, DAMAGES (INCLUDING, WITHOUT LIMITATION, DIRECT, INDIRECT OR CONSEQUENTIAL DAMAGES) OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/mobile/android/lib/cbl_collator_so-1.0.0.jar b/mobile/android/lib/cbl_collator_so-1.0.2.jar similarity index 96% rename from mobile/android/lib/cbl_collator_so-1.0.0.jar rename to mobile/android/lib/cbl_collator_so-1.0.2.jar index bb9e100..92b50f7 100644 Binary files a/mobile/android/lib/cbl_collator_so-1.0.0.jar and b/mobile/android/lib/cbl_collator_so-1.0.2.jar differ diff --git a/mobile/android/lib/couchbase-lite-android-1.0.0.jar b/mobile/android/lib/couchbase-lite-android-1.0.2.jar similarity index 63% rename from mobile/android/lib/couchbase-lite-android-1.0.0.jar rename to mobile/android/lib/couchbase-lite-android-1.0.2.jar index 23cd705..b804ca3 100644 Binary files a/mobile/android/lib/couchbase-lite-android-1.0.0.jar and b/mobile/android/lib/couchbase-lite-android-1.0.2.jar differ diff --git a/mobile/android/lib/couchbase-lite-java-core-1.0.0.jar b/mobile/android/lib/couchbase-lite-java-core-1.0.0.jar deleted file mode 100644 index e8d05fb..0000000 Binary files a/mobile/android/lib/couchbase-lite-java-core-1.0.0.jar and /dev/null differ diff --git a/mobile/android/lib/couchbase-lite-java-core-1.0.2.jar b/mobile/android/lib/couchbase-lite-java-core-1.0.2.jar new file mode 100644 index 0000000..c05d9df Binary files /dev/null and b/mobile/android/lib/couchbase-lite-java-core-1.0.2.jar differ diff --git a/mobile/android/lib/couchbase-lite-java-javascript-1.0.0.jar b/mobile/android/lib/couchbase-lite-java-javascript-1.0.2.jar similarity index 82% rename from mobile/android/lib/couchbase-lite-java-javascript-1.0.0.jar rename to mobile/android/lib/couchbase-lite-java-javascript-1.0.2.jar index fba988c..11ed216 100644 Binary files a/mobile/android/lib/couchbase-lite-java-javascript-1.0.0.jar and b/mobile/android/lib/couchbase-lite-java-javascript-1.0.2.jar differ diff --git a/mobile/android/lib/couchbase-lite-java-listener-1.0.2.jar b/mobile/android/lib/couchbase-lite-java-listener-1.0.2.jar new file mode 100644 index 0000000..d9aee44 Binary files /dev/null and b/mobile/android/lib/couchbase-lite-java-listener-1.0.2.jar differ diff --git a/mobile/android/lib/webserver-2-3.jar b/mobile/android/lib/webserver-2-3.jar new file mode 100644 index 0000000..ea31895 Binary files /dev/null and b/mobile/android/lib/webserver-2-3.jar differ diff --git a/mobile/android/manifest b/mobile/android/manifest index bd362ef..ed4d8fa 100644 --- a/mobile/android/manifest +++ b/mobile/android/manifest @@ -2,7 +2,7 @@ # this is your module manifest and used by Titanium # during compilation, packaging, distribution, etc. # -version: 1.0.1 +version: 1.1.0 apiversion: 2 description: TouchDB for Titanium author: Paul Mietz Egli diff --git a/mobile/android/src/com/obscure/titouchdb/AttachmentProxy.java b/mobile/android/src/com/obscure/titouchdb/AttachmentProxy.java index 4f658d2..45f68fc 100644 --- a/mobile/android/src/com/obscure/titouchdb/AttachmentProxy.java +++ b/mobile/android/src/com/obscure/titouchdb/AttachmentProxy.java @@ -12,6 +12,8 @@ import android.util.Log; import com.couchbase.lite.Attachment; +import com.couchbase.lite.BlobStore; +import com.couchbase.lite.BlobStoreWriter; import com.couchbase.lite.CouchbaseLiteException; @Kroll.proxy(parentModule = TitouchdbModule.class) @@ -65,6 +67,12 @@ public TiBlob getContent() { public String getContentType() { return attachment.getContentType(); } + + @Kroll.getProperty(name = "contentURL") + public String getContentURL() { + BlobStore store = new BlobStore(attachment.getDocument().getDatabase().getAttachmentStorePath()); + return "file:/" + store.pathForKey(BlobStore.keyForBlob(getContent().getBytes())); + } @Kroll.getProperty(name = "document") public DocumentProxy getDocument() { diff --git a/mobile/android/src/com/obscure/titouchdb/AuthenticatorProxy.java b/mobile/android/src/com/obscure/titouchdb/AuthenticatorProxy.java new file mode 100644 index 0000000..2bff399 --- /dev/null +++ b/mobile/android/src/com/obscure/titouchdb/AuthenticatorProxy.java @@ -0,0 +1,23 @@ +package com.obscure.titouchdb; + +import org.appcelerator.kroll.KrollProxy; +import org.appcelerator.kroll.annotations.Kroll; + +import com.couchbase.lite.auth.Authenticator; + +@Kroll.proxy(parentModule = TitouchdbModule.class) +public class AuthenticatorProxy extends KrollProxy { + + private static final String LCAT = "AuthenticatorProxy"; + + private Authenticator authenticator; + + public AuthenticatorProxy(Authenticator authenticator) { + this.authenticator = authenticator; + } + + public Authenticator getAuthenticator() { + return authenticator; + } + +} diff --git a/mobile/android/src/com/obscure/titouchdb/DatabaseProxy.java b/mobile/android/src/com/obscure/titouchdb/DatabaseProxy.java index af79322..0411712 100644 --- a/mobile/android/src/com/obscure/titouchdb/DatabaseProxy.java +++ b/mobile/android/src/com/obscure/titouchdb/DatabaseProxy.java @@ -124,6 +124,12 @@ public ReplicationProxy createPushReplication(String url) { return result; } + @Kroll.method + public QueryProxy createSlowQuery(KrollFunction map) { + lastError = null; + return new QueryProxy(this, database.slowQuery(new KrollMapper(map))); + } + @Kroll.method public boolean deleteDatabase() { lastError = null; diff --git a/mobile/android/src/com/obscure/titouchdb/ReplicationProxy.java b/mobile/android/src/com/obscure/titouchdb/ReplicationProxy.java index 482d8c0..5b214f3 100644 --- a/mobile/android/src/com/obscure/titouchdb/ReplicationProxy.java +++ b/mobile/android/src/com/obscure/titouchdb/ReplicationProxy.java @@ -21,6 +21,8 @@ public class ReplicationProxy extends KrollProxy implements ChangeListener { private static final String LCAT = "ReplicationProxy"; + private AuthenticatorProxy authenticatorProxy; + private DatabaseProxy databaseProxy; private KrollDict lastError = null; @@ -36,6 +38,20 @@ public ReplicationProxy(DatabaseProxy databaseProxy, Replication replicator) { replicator.addChangeListener(this); } + @Override + public void changed(ChangeEvent e) { + KrollDict params = new KrollDict(); + params.put("source", this); + params.put("status", replicator.getStatus().ordinal()); + + fireEvent("change", params); + } + + @Kroll.getProperty(name="authenticator") + public AuthenticatorProxy getAuthenticator() { + return authenticatorProxy; + } + @Kroll.getProperty(name = "changesCount") public int getChangesCount() { return replicator.getChangesCount(); @@ -113,6 +129,12 @@ public void restart() { replicator.restart(); } + @Kroll.setProperty(name="authenticator") + public void setAuthenticator(AuthenticatorProxy authenticatorProxy) { + this.authenticatorProxy = authenticatorProxy; + replicator.setAuthenticator(authenticatorProxy != null ? authenticatorProxy.getAuthenticator() : null); + } + @Kroll.setProperty(name = "continuous") public void setContinuous(boolean continuous) { replicator.setContinuous(continuous); @@ -124,7 +146,7 @@ public void setCreateTarget(boolean createTarget) { } @Kroll.method - public void setCredential(@Kroll.argument(optional=true) KrollDict credential) { + public void setCredential(@Kroll.argument(optional = true) KrollDict credential) { if (credential == null) { replicator.setAuthenticator(null); } @@ -167,13 +189,4 @@ public void stop() { replicator.stop(); } - @Override - public void changed(ChangeEvent e) { - KrollDict params = new KrollDict(); - params.put("source", this); - params.put("status", replicator.getStatus().ordinal()); - - fireEvent("status", params); - } - } \ No newline at end of file diff --git a/mobile/android/src/com/obscure/titouchdb/TitouchdbModule.java b/mobile/android/src/com/obscure/titouchdb/TitouchdbModule.java index 3947a66..523b476 100644 --- a/mobile/android/src/com/obscure/titouchdb/TitouchdbModule.java +++ b/mobile/android/src/com/obscure/titouchdb/TitouchdbModule.java @@ -22,6 +22,7 @@ import com.couchbase.lite.Query; import com.couchbase.lite.SavedRevision; import com.couchbase.lite.Status; +import com.couchbase.lite.auth.AuthenticatorFactory; @Kroll.module(name = "Titouchdb", id = "com.obscure.titouchdb") public class TitouchdbModule extends KrollModule { @@ -116,11 +117,26 @@ public TitouchdbModule() { Log.i(LCAT, this.toString() + " loaded"); } + @Kroll.method + public AuthenticatorProxy createBasicAuthenticator(String username, String password) { + return new AuthenticatorProxy(AuthenticatorFactory.createBasicAuthenticator(username, password)); + } + + @Kroll.method + public AuthenticatorProxy createFacebookAuthenticator(String token) { + return new AuthenticatorProxy(AuthenticatorFactory.createFacebookAuthenticator(token)); + } + + @Kroll.method + public AuthenticatorProxy createPersonaAuthenticator(String assertion, @Kroll.argument(optional=true) String email) { + return new AuthenticatorProxy(AuthenticatorFactory.createPersonaAuthenticator(assertion, email)); + } + @Kroll.getProperty(name = "databaseManager") public DatabaseManagerProxy getDatabaseManager() { return this.databaseManagerProxy; } - + @Override protected void initActivity(Activity activity) { super.initActivity(activity); diff --git a/mobile/ios/.gitignore b/mobile/ios/.gitignore index 12dd3c2..1549bc6 100644 --- a/mobile/ios/.gitignore +++ b/mobile/ios/.gitignore @@ -3,3 +3,5 @@ bin build *.zip example +documentation +.* diff --git a/mobile/ios/Classes/ComObscureTitouchdbModule.m b/mobile/ios/Classes/ComObscureTitouchdbModule.m index a3fd7c6..4b2e57c 100644 --- a/mobile/ios/Classes/ComObscureTitouchdbModule.m +++ b/mobile/ios/Classes/ComObscureTitouchdbModule.m @@ -14,6 +14,7 @@ #import "TiUtils.h" #import "TiProxy+Errors.h" #import "TDDatabaseManagerProxy.h" +#import "TDAuthenticatorProxy.h" extern BOOL EnableLog(BOOL enable); @@ -39,7 +40,7 @@ -(NSString*)moduleId { -(void)startup { [super startup]; - EnableLog(YES); + [CBLManager enableLogging:nil]; NSLog(@"[INFO] %@ loaded", self); @@ -122,6 +123,33 @@ - (id)stopListener:(id)args { [self.listener stop]; } +#pragma mark - +#pragma mark CBLAuthenticator + +- (id)createBasicAuthenticator:(id)args { + NSString * user; + NSString * pass; + ENSURE_ARG_AT_INDEX(user, args, 0, NSString) + ENSURE_ARG_AT_INDEX(pass, args, 1, NSString) + + return [TDAuthenticatorProxy proxyWithAuthenticator:[CBLAuthenticator basicAuthenticatorWithName:user password:pass]]; +} + +- (id)createFacebookAuthenticator:(id)args { + NSString * token; + ENSURE_ARG_AT_INDEX(token, args, 0, NSString) + + return [TDAuthenticatorProxy proxyWithAuthenticator:[CBLAuthenticator facebookAuthenticatorWithToken:token]]; +} + +- (id)createPersonaAuthenticator:(id)args { + NSString * assertion; + ENSURE_ARG_AT_INDEX(assertion, args, 0, NSString) + + return [TDAuthenticatorProxy proxyWithAuthenticator:[CBLAuthenticator personaAuthenticatorWithAssertion:assertion]]; +} + + #pragma mark - #pragma mark Constants diff --git a/mobile/ios/Classes/TDAuthenticatorProxy.h b/mobile/ios/Classes/TDAuthenticatorProxy.h new file mode 100644 index 0000000..0199916 --- /dev/null +++ b/mobile/ios/Classes/TDAuthenticatorProxy.h @@ -0,0 +1,15 @@ +// +// TDAuthenticatorProxy.h +// titouchdb +// +// Created by Paul Mietz Egli on 7/14/14. +// +// + +#import "TiProxy.h" +#import "CBLAuthenticator.h" + +@interface TDAuthenticatorProxy : TiProxy +@property (nonatomic, strong) id authenticator; ++ (instancetype)proxyWithAuthenticator:(id)authenticator; +@end diff --git a/mobile/ios/Classes/TDAuthenticatorProxy.m b/mobile/ios/Classes/TDAuthenticatorProxy.m new file mode 100644 index 0000000..ef76efe --- /dev/null +++ b/mobile/ios/Classes/TDAuthenticatorProxy.m @@ -0,0 +1,24 @@ +// +// TDAuthenticatorProxy.m +// titouchdb +// +// Created by Paul Mietz Egli on 7/14/14. +// +// + +#import "TDAuthenticatorProxy.h" + +@implementation TDAuthenticatorProxy + ++ (instancetype)proxyWithAuthenticator:(id)authenticator { + return [[TDAuthenticatorProxy alloc] initWithAuthenticator:authenticator]; +} + +- (id)initWithAuthenticator:(id)authenticator { + if (self = [super init]) { + self.authenticator = authenticator; + } + return self; +} + +@end diff --git a/mobile/ios/Classes/TDDatabaseManagerProxy.m b/mobile/ios/Classes/TDDatabaseManagerProxy.m index 42ff21a..e092383 100644 --- a/mobile/ios/Classes/TDDatabaseManagerProxy.m +++ b/mobile/ios/Classes/TDDatabaseManagerProxy.m @@ -182,4 +182,13 @@ - (id)error { return self.lastError ? [self errorDict:self.lastError] : nil; } +/** specify whether the CouchbaseLite directory should be backed up or not */ +- (void)setExcludedFromBackup:(id)value { + [self.databaseManager setExcludedFromBackup:[value boolValue]]; +} + +- (id)excludedFromBackup { + return NUMBOOL(self.databaseManager.excludedFromBackup); +} + @end diff --git a/mobile/ios/Classes/TDDatabaseProxy.m b/mobile/ios/Classes/TDDatabaseProxy.m index 35447aa..3a833c2 100644 --- a/mobile/ios/Classes/TDDatabaseProxy.m +++ b/mobile/ios/Classes/TDDatabaseProxy.m @@ -189,7 +189,7 @@ - (id)createAllDocumentsQuery:(id)args { return [TDQueryProxy proxyWithDatabase:self query:query]; } -- (id)slowQueryWithMap:(id)args { +- (id)createSlowQuery:(id)args { KrollCallback * callback; ENSURE_ARG_AT_INDEX(callback, args, 0, KrollCallback); diff --git a/mobile/ios/Classes/TDQueryProxy.m b/mobile/ios/Classes/TDQueryProxy.m index efa1e0b..a9c0377 100644 --- a/mobile/ios/Classes/TDQueryProxy.m +++ b/mobile/ios/Classes/TDQueryProxy.m @@ -46,6 +46,11 @@ - (id)initWithExecutionContext:(id)context CBLQuery:(CBLQuery *)que return self; } +- (void)dealloc { + self.query = nil; + [super dealloc]; +} + #pragma mark Properties - (id)limit { @@ -187,6 +192,11 @@ - (id)initWithQuery:(TDQueryProxy *) query queryEnumerator:(CBLQueryEnumerator * return self; } +- (void)dealloc { + self.enumerator = nil; + [super dealloc]; +} + - (id)count { return NUMLONG(self.enumerator.count); } @@ -238,6 +248,11 @@ - (id)initWithQueryEnumerator:(TDQueryEnumeratorProxy *)enumerator queryRow:(CBL return self; } +- (void)dealloc { + self.row = nil; + [super dealloc]; +} + - (id)database { return self.queryEnumerator.query.database; } diff --git a/mobile/ios/Classes/TDReplicationProxy.m b/mobile/ios/Classes/TDReplicationProxy.m index 3482212..d7853f8 100644 --- a/mobile/ios/Classes/TDReplicationProxy.m +++ b/mobile/ios/Classes/TDReplicationProxy.m @@ -8,6 +8,7 @@ #import "TDReplicationProxy.h" #import "TDDatabaseProxy.h" +#import "TDAuthenticatorProxy.h" #import "TiProxy+Errors.h" extern NSString * CBL_ReplicatorProgressChangedNotification; @@ -16,6 +17,7 @@ @interface TDReplicationProxy () @property (nonatomic, assign) TDDatabaseProxy * database; @property (nonatomic, strong) CBLReplication * replication; +@property (nonatomic, strong) TDAuthenticatorProxy * authenticatorProxy; - (void)startObservingReplication:(CBLReplication*)repl; - (void)stopObservingReplication:(CBLReplication*)repl; @end @@ -120,6 +122,15 @@ - (void)setNetwork:(id)value { #pragma mark Authentication +- (void)setAuthenticator:(id)value { + self.authenticatorProxy = value; + self.replication.authenticator = self.authenticatorProxy.authenticator; +} + +- (id)authenticator { + return self.authenticatorProxy; +} + - (void)setCredential:(id)value { ENSURE_DICT(value) NSDictionary * cred = value; @@ -163,34 +174,28 @@ - (id)status { #pragma mark Notifications #define kReplicationChangedEventName @"change" -#define kReplicationStoppedEventName @"status" - (void)startObservingReplication:(CBLReplication*)repl { [repl addObserver:self forKeyPath:@"completedChangesCount" options:0 context:NULL]; [repl addObserver:self forKeyPath:@"changesCount" options:0 context:NULL]; [repl addObserver:self forKeyPath:@"status" options:0 context:NULL]; + [repl addObserver:self forKeyPath:@"running" options:0 context:NULL]; + [repl addObserver:self forKeyPath:@"lastError" options:0 context:NULL]; } - (void)stopObservingReplication:(CBLReplication*)repl { [repl removeObserver:self forKeyPath:@"completedChangesCount"]; [repl removeObserver:self forKeyPath:@"changesCount"]; [repl removeObserver:self forKeyPath:@"status"]; + [repl removeObserver:self forKeyPath:@"running"]; + [repl removeObserver:self forKeyPath:@"lastError"]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { CBLReplication * repl = (CBLReplication *)object; - if ([@"status" isEqualToString:keyPath]) { - // fire status event - TiThreadPerformOnMainThread(^{ - [self fireEvent:kReplicationStoppedEventName withObject:@{@"status":NUMINT(repl.status)} propagate:YES]; - }, NO); - } - else { - // fire change event - TiThreadPerformOnMainThread(^{ - [self fireEvent:kReplicationChangedEventName withObject:nil propagate:YES]; - }, NO); - } + TiThreadPerformOnMainThread(^{ + [self fireEvent:kReplicationChangedEventName withObject:@{@"property": keyPath, @"source": self} propagate:YES]; + }, NO); } @end diff --git a/mobile/ios/Classes/TiProxy+Errors.m b/mobile/ios/Classes/TiProxy+Errors.m index be75a1f..71d88cf 100644 --- a/mobile/ios/Classes/TiProxy+Errors.m +++ b/mobile/ios/Classes/TiProxy+Errors.m @@ -10,15 +10,15 @@ @implementation TiProxy (Errors) -- (NSDictionary *)errorDict:(NSError *)error { - NSMutableDictionary * result = nil; +- (id)errorDict:(NSError *)error { + NSDictionary * result = nil; if (error) { - result = [NSMutableDictionary dictionaryWithObjectsAndKeys: - NUMBOOL(YES), @"error", - NUMINT(error.code), @"code", - error.domain, @"domain", - error.localizedDescription, @"description", - nil]; + result = @{ + @"error": NUMBOOL(YES), + @"code": NUMLONG(error.code), + @"domain": error.domain, + @"description": error.localizedDescription + }; /* // who knows what evil lurks in the hearts of error.userInfo? // whatever it is, it can't be serialized to javascript... @@ -27,12 +27,7 @@ - (NSDictionary *)errorDict:(NSError *)error { } */ } - /* - else { - result = [NSMutableDictionary dictionaryWithObject:NUMBOOL(NO) forKey:@"error"]; - } - */ - return result; + return result ? [result autorelease] : [NSNull null]; } @end diff --git a/mobile/ios/dist/com.obscure.titouchdb-iphone-1.0.zip b/mobile/ios/dist/com.obscure.titouchdb-iphone-1.0.zip deleted file mode 100644 index de284c7..0000000 Binary files a/mobile/ios/dist/com.obscure.titouchdb-iphone-1.0.zip and /dev/null differ diff --git a/mobile/ios/manifest b/mobile/ios/manifest index e05898d..4d9fbc5 100644 --- a/mobile/ios/manifest +++ b/mobile/ios/manifest @@ -2,7 +2,7 @@ # this is your module manifest and used by Titanium # during compilation, packaging, distribution, etc. # -version: 1.0.2 +version: 1.1.0 apiversion: 2 description: Titanium wrapper for Couchbase Mobile author: Paul Mietz Egli diff --git a/mobile/ios/titouchdb.xcodeproj/project.pbxproj b/mobile/ios/titouchdb.xcodeproj/project.pbxproj index d7d41bd..d4f41b1 100644 --- a/mobile/ios/titouchdb.xcodeproj/project.pbxproj +++ b/mobile/ios/titouchdb.xcodeproj/project.pbxproj @@ -46,6 +46,8 @@ DAA36892167F9BB6004A514D /* TDReplicationProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = DA4919431676B78A00ECCB83 /* TDReplicationProxy.m */; }; DAA36893167F9BB6004A514D /* TDAttachmentProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = DABFA05116779B4100A58019 /* TDAttachmentProxy.m */; }; DABFA05216779B4200A58019 /* TDAttachmentProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = DABFA05016779B4100A58019 /* TDAttachmentProxy.h */; }; + DAD03A8A197449FA00886829 /* TDAuthenticatorProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = DAD03A88197449FA00886829 /* TDAuthenticatorProxy.h */; }; + DAD03A8B197449FA00886829 /* TDAuthenticatorProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = DAD03A89197449FA00886829 /* TDAuthenticatorProxy.m */; }; DAE72A37152E66C900622013 /* TiProxy+Errors.h in Headers */ = {isa = PBXBuildFile; fileRef = DAE72A35152E66C800622013 /* TiProxy+Errors.h */; }; DAEE1D1A1932850500ABEF1D /* libCouchbaseLite.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DA77FA4B1932849500D85AEE /* libCouchbaseLite.a */; }; DAEE1D1B1932850500ABEF1D /* libCouchbaseLiteListener.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DA77FA571932849500D85AEE /* libCouchbaseLiteListener.a */; }; @@ -192,6 +194,8 @@ DA77FA351932849400D85AEE /* CouchbaseLite.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = CouchbaseLite.xcodeproj; path = "vendor/couchbase-lite-ios/CouchbaseLite.xcodeproj"; sourceTree = ""; }; DABFA05016779B4100A58019 /* TDAttachmentProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TDAttachmentProxy.h; path = Classes/TDAttachmentProxy.h; sourceTree = ""; }; DABFA05116779B4100A58019 /* TDAttachmentProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TDAttachmentProxy.m; path = Classes/TDAttachmentProxy.m; sourceTree = ""; }; + DAD03A88197449FA00886829 /* TDAuthenticatorProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TDAuthenticatorProxy.h; path = Classes/TDAuthenticatorProxy.h; sourceTree = ""; }; + DAD03A89197449FA00886829 /* TDAuthenticatorProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TDAuthenticatorProxy.m; path = Classes/TDAuthenticatorProxy.m; sourceTree = ""; }; DAE72A35152E66C800622013 /* TiProxy+Errors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "TiProxy+Errors.h"; path = "Classes/TiProxy+Errors.h"; sourceTree = ""; }; DAE72A36152E66C800622013 /* TiProxy+Errors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "TiProxy+Errors.m"; path = "Classes/TiProxy+Errors.m"; sourceTree = ""; }; DAF6E66D1680F84D003217C3 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; @@ -279,6 +283,8 @@ DA4919431676B78A00ECCB83 /* TDReplicationProxy.m */, DABFA05016779B4100A58019 /* TDAttachmentProxy.h */, DABFA05116779B4100A58019 /* TDAttachmentProxy.m */, + DAD03A88197449FA00886829 /* TDAuthenticatorProxy.h */, + DAD03A89197449FA00886829 /* TDAuthenticatorProxy.m */, ); name = Classes; sourceTree = ""; @@ -329,6 +335,7 @@ DA49193C1676ADF100ECCB83 /* TDViewProxy.h in Headers */, DA4919401676B32800ECCB83 /* TDRevisionProxy.h in Headers */, DA4919441676B78B00ECCB83 /* TDReplicationProxy.h in Headers */, + DAD03A8A197449FA00886829 /* TDAuthenticatorProxy.h in Headers */, DABFA05216779B4200A58019 /* TDAttachmentProxy.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -505,6 +512,7 @@ DAA3688D167F9BB6004A514D /* TDDocumentProxy.m in Sources */, DAA3688E167F9BB6004A514D /* TDQueryProxy.m in Sources */, DAA3688F167F9BB6004A514D /* TDBridge.m in Sources */, + DAD03A8B197449FA00886829 /* TDAuthenticatorProxy.m in Sources */, DAA36890167F9BB6004A514D /* TDViewProxy.m in Sources */, DAA36891167F9BB6004A514D /* TDRevisionProxy.m in Sources */, DAA36892167F9BB6004A514D /* TDReplicationProxy.m in Sources */, diff --git a/mobile/ios/vendor/couchbase-lite-ios b/mobile/ios/vendor/couchbase-lite-ios index 2d3a97d..b3c92e2 160000 --- a/mobile/ios/vendor/couchbase-lite-ios +++ b/mobile/ios/vendor/couchbase-lite-ios @@ -1 +1 @@ -Subproject commit 2d3a97dd4a2881a81cc6d46bd75e8f0646ea32f8 +Subproject commit b3c92e25d0e883d72d7144c15642a531f39e526d diff --git a/mobile/noarch/alloy/sync/titouchdb.js b/mobile/noarch/alloy/sync/titouchdb.js index c4fa773..438fb21 100755 --- a/mobile/noarch/alloy/sync/titouchdb.js +++ b/mobile/noarch/alloy/sync/titouchdb.js @@ -3,6 +3,7 @@ */ var _ = require('alloy/underscore'), + moment = require('alloy/moment'), titouchdb = require('com.obscure.titouchdb'), manager = titouchdb.databaseManager, db; @@ -26,7 +27,7 @@ function query_view(db, name, options) { } return null; } - + if (_.isBoolean(opts.prefetch)) { query.prefetch = opts.prefetch; } if (_.isFinite(opts.limit)) { query.limit = opts.limit; } if (_.isFinite(opts.skip)) { query.skip = opts.skip; } @@ -65,10 +66,11 @@ function InitAdapter(config) { db = manager.getDatabase(config.adapter.dbname); // register views - _.each(config.adapter.views, function(view) { - var v = db.getView(view.name); - v.setMapReduce(view.map, view.reduce, view.version || '1'); - Ti.API.info("defined "+view.name); + _.each(_.keys(config.adapter.views), function(name) { + var def = config.adapter.views[name]; + var v = db.getView(name); + v.setMapReduce(def.map, def.reduce, def.version || '1'); + Ti.API.info("defined "+name); }); return {}; @@ -82,7 +84,10 @@ function Sync(method, model, options) { switch (method) { case 'create': var props = model.toJSON(); - _.extend(props, model.config.adapter.static_properties || {}); + props = _.defaults(props, + model.config.adapter.static_properties || {}, + model.config.adapter.modelname ? { modelname: model.config.adapter.modelname } : {} + ); var doc = model.id ? db.getDocument(model.id) : db.createDocument(); doc.putProperties(props); err = doc.error; @@ -111,12 +116,16 @@ function Sync(method, model, options) { var collection = model; // just to clear things up // collection - var view = opts.view || collection.config.adapter.views[0]["name"]; + var view = collection.config.adapter.views[opts.view]; + if (!view) { + err = "missing view named " + opts.view; + break; + } collection.view = view; // add default view options from model - opts = _.defaults(opts, collection.config.adapter.view_options); - var query = query_view(db, view, opts); + opts = _.defaults(opts, view.query_options || {}, collection.config.adapter.default_query_options); + var query = query_view(db, opts.view, opts); if (!query) { err = { error: 'missing view' }; break; @@ -144,7 +153,10 @@ function Sync(method, model, options) { case 'update': var props = model.toJSON(); - _.extend(props, model.config.adapter.static_properties || {}); + props = _.defaults(props, + model.config.adapter.static_properties || {}, + model.config.adapter.modelname ? { modelname: model.config.adapter.modelname } : {} + ); var doc = db.getDocument(model.id); doc.putProperties(props); err = doc.error; @@ -180,21 +192,139 @@ function Sync(method, model, options) { module.exports.sync = Sync; -module.exports.beforeModelCreate = function(config) { - config = config || {}; +// MIGRATIONS + +var migration_doc_name = 'titouchdb_migrations'; + +function Migrator(config) { + this.dbname = config.adapter.db_name; + this.idAttribute = config.adapter.idAttribute; + this.database = db; // TODO ensure db is set? - InitAdapter(config); + // ensure that the properties defined in the model config are set + this.createModel = function(props) { + var doc = props.id ? this.database.getDocument(props.id) : this.database.createDocument(); + props = _.defaults(props, + config.adapter.static_properties || {}, + config.adapter.modelname ? { modelname: config.adapter.modelname } : {} + ); + doc.putProperties(props); + }; +} + +// Gets the current saved migration +function GetLatestMigration(database) { + var mdoc = database.getExistingDocument(migration_doc_name); + return mdoc ? mdoc.userProperties.id : null; +} + +function Migrate(Model) { + // get list of migrations for this model + var migrations = Model.migrations || []; + + // get a reference to the last migration + var lastMigration = {}; + if (migrations.length) { migrations[migrations.length-1](lastMigration); } + + // Get config reference + var config = Model.prototype.config; + + // Set up the migration obejct + var migrator = new Migrator(config); + + // Get the migration number from the config, or use the number of + // the last migration if it's not present. If we still don't have a + // migration number after that, that means there are none. There's + // no migrations to perform. + var targetNumber = typeof config.adapter.migration === 'undefined' || + config.adapter.migration === null ? lastMigration.id : config.adapter.migration; + if (typeof targetNumber === 'undefined' || targetNumber === null) { + return; + } + targetNumber = targetNumber + ''; // ensure that it's a string + + // Get the current saved migration number. + var currentNumber = GetLatestMigration(db); + + // If the current and requested migrations match, the data structures + // match and there is no need to run the migrations. + var direction; + if (currentNumber === targetNumber) { + return; + } else if (currentNumber && currentNumber > targetNumber) { + direction = 0; // rollback + migrations.reverse(); + } else { + direction = 1; // upgrade + } + + migrator.database = db; + + // iterate through all migrations based on the current and requested state, + // applying all appropriate migrations, in order, to the database. + var lastContext; + if (migrations.length) { + for (var i = 0; i < migrations.length; i++) { + // create the migration context + var migration = migrations[i]; + var context = {}; + migration(context); + // if upgrading, skip migrations higher than the target + // if rolling back, skip migrations lower than the target + if (direction) { + if (context.id > targetNumber) { break; } + if (context.id <= currentNumber) { continue; } + } else { + if (context.id <= targetNumber) { break; } + if (context.id > currentNumber) { continue; } + } + + // execute the appropriate migration function + var funcName = direction ? 'up' : 'down'; + if (_.isFunction(context[funcName])) { + context[funcName](migrator); + lastContext = context; + } + } + } + + // insert a doc to track this migration + if (lastContext) { + var mdoc = db.getDocument(migration_doc_name); + mdoc.putProperties({ + id: lastContext.id, + name: lastContext.name, + applied: moment().unix() + }); + } + + migrator.db = null; +} + +// EXPORTED FUNCTIONS + +var cache = { + config: {}, + Model: {} +}; + +module.exports.beforeModelCreate = function(config, name) { + if (cache.config[name]) return cache.config[name]; + config = config || {}; + InitAdapter(config); + cache.config[name] = config; return config; }; -module.exports.afterModelCreate = function(Model) { - Model = Model || {}; +module.exports.afterModelCreate = function(Model, name) { + if (cache.Model[name]) { + return cache.Model[name]; + } + Model = Model || {}; Model.prototype.idAttribute = '_id'; // true for all TouchDB documents - Model.prototype.config.Model = Model; // needed for fetch operations to initialize the collection from persistent store - Model.prototype.database = db; - + Model.prototype.attachmentNamed = function(name) { var doc = db.getDocument(this.id); if (doc) { @@ -225,6 +355,19 @@ module.exports.afterModelCreate = function(Model) { return doc ? doc.currentRevision.attachmentNames : []; }; + Migrate(Model); + + cache.Model[name] = Model; + return Model; }; +/* +module.exports.afterCollectionCreate = function(Collection) { + Collection = Collection || {}; + + Collection.prototype.database = db; + + return Collection; +}; +*/ \ No newline at end of file diff --git a/mobile/noarch/documentation/changes.md b/mobile/noarch/documentation/changes.md index 74ffeb6..4f3c64c 100644 --- a/mobile/noarch/documentation/changes.md +++ b/mobile/noarch/documentation/changes.md @@ -1,3 +1,18 @@ +2014-08-26 + +* Updated to Couchbase Mobile 1.0.2 release for Android and iOS +* Fixed memory leak of CBL query objects (issue 80) +* Modified the Alloy sync adapter configuration to allow specifying + query properties with the view definition. +* Added data model migration support to the sync adapter. + + +2014-07-30 + +### Database + +* added `createSlowQuery(mapfn)` method. + 2014-06-10 Initial release of module based on [couchbase-lite-ios](https://github.com/couchbase/couchbase-lite-ios) diff --git a/mobile/noarch/documentation/index.md b/mobile/noarch/documentation/index.md index aa5f345..591182b 100644 --- a/mobile/noarch/documentation/index.md +++ b/mobile/noarch/documentation/index.md @@ -216,6 +216,14 @@ object. The returned object can be customized prior to the start of replication Set up a one-time replication from this database to a remote target database and return a [`replication`](#replication) object. The returned object can be customized prior to the start of replication. +**createSlowQuery**(map) + +* map (function(document)): the map function used to create the query + +Create a new [`query`](#query) object with the provided map function. The query index will be created once +when the query is run but not persisted. This function is best used for development only or in cases where +creating a persistent view is not desirable. + **deleteDatabase**() Permanently delete this database and all of its documents. diff --git a/mobile/noarch/example/.gitignore b/mobile/noarch/example/.gitignore index ce392ae..bfdbfc5 100644 --- a/mobile/noarch/example/.gitignore +++ b/mobile/noarch/example/.gitignore @@ -1,3 +1,3 @@ -replication_config.json +replication_config.json* LiteServ/bin/* LiteServ/Frameworks/* diff --git a/mobile/noarch/example/001_module.js b/mobile/noarch/example/001_module.js index 059cc9d..d0bf566 100644 --- a/mobile/noarch/example/001_module.js +++ b/mobile/noarch/example/001_module.js @@ -3,8 +3,9 @@ require('ti-mocha'); var should = require('should'); module.exports = function() { + var titouchdb = require('com.obscure.titouchdb'); + describe('module', function() { - var titouchdb = require('com.obscure.titouchdb'); it('must exist', function() { should.exist(titouchdb); @@ -36,4 +37,17 @@ module.exports = function() { }); + describe('module (authenticators)', function() { + it('must have a createBasicAuthenticator method', function() { + should(titouchdb.createBasicAuthenticator).be.a.Function; + }); + + it('must have a createFacebookAuthenticator method', function() { + should(titouchdb.createFacebookAuthenticator).be.a.Function; + }); + + it('must have a createPersonaAuthenticator method', function() { + should(titouchdb.createPersonaAuthenticator).be.a.Function; + }); + }); }; \ No newline at end of file diff --git a/mobile/noarch/example/002_databaseManager.js b/mobile/noarch/example/002_databaseManager.js index 273b1a9..fa1cd5e 100644 --- a/mobile/noarch/example/002_databaseManager.js +++ b/mobile/noarch/example/002_databaseManager.js @@ -65,7 +65,7 @@ module.exports = function() { should(manager.getExistingDatabase).be.a.Function; var db = manager.getExistingDatabase('test_does_not_exist'); should.not.exist(db); - should(manager.error).be.an.Object + should(manager.error).be.an.Object; }); it('must return a previously created database', function() { diff --git a/mobile/noarch/example/004_all_documents_query.js b/mobile/noarch/example/004_all_documents_query.js index b164291..2119aa6 100644 --- a/mobile/noarch/example/004_all_documents_query.js +++ b/mobile/noarch/example/004_all_documents_query.js @@ -56,4 +56,27 @@ module.exports = function() { }); + describe('database (slow query)', function() { + var db; + + before(function() { + utils.delete_nonsystem_databases(manager); + db = utils.install_elements_database(manager); + }); + + it('must run a slow query', function() { + var q = db.createSlowQuery(function(doc) { + if (['Pd', 'Ag', 'Pt', 'Au'].indexOf(doc.symbol) !== -1) { + emit(doc.symbol, null); + } + }); + + var e = q.run(); + e.count.should.eql(4); + e.getRow(0).key.should.eql('Ag'); + e.getRow(1).key.should.eql('Au'); + e.getRow(2).key.should.eql('Pd'); + e.getRow(3).key.should.eql('Pt'); + }); + }); }; diff --git a/mobile/noarch/example/005_database_validation.js b/mobile/noarch/example/005_database_validation.js index aeb9248..8dded03 100644 --- a/mobile/noarch/example/005_database_validation.js +++ b/mobile/noarch/example/005_database_validation.js @@ -13,7 +13,7 @@ module.exports = function() { var dummy_fn = function() {}; before(function() { - utils.delete_nonsystem_databases(manager) + utils.delete_nonsystem_databases(manager); db = manager.getDatabase('test005_validation'); db.setValidation('require_tag', function(rev, context) { if (rev.properties.tag == null) { @@ -66,7 +66,7 @@ module.exports = function() { it('must allow validation functions that call other functions', function() { var is_int = function(n, v) { return n != null && v != null && !isNaN(parseInt(v)); - } + }; db.setValidation('tag_must_be_int', function(rev, context) { if (!is_int('tag', rev.properties.tag)) { diff --git a/mobile/noarch/example/008_attachments.js b/mobile/noarch/example/008_attachments.js index fb54f25..6e453d1 100644 --- a/mobile/noarch/example/008_attachments.js +++ b/mobile/noarch/example/008_attachments.js @@ -53,4 +53,24 @@ module.exports = function() { }); }); + + // properties and methods that are not part of the common API + describe('attachment (extended)', function() { + var db, doc, att; + + before(function() { + utils.delete_nonsystem_databases(manager); + db = utils.install_elements_database(manager); + + doc = db.getExistingDocument('Bi'); + att = doc.currentRevision.getAttachment('image.jpg'); + }); + + it('must have a contentURL property', function() { + should(att).have.property('contentURL'); + var f = Ti.Filesystem.getFile(att.contentURL); + should.exist(f); + f.exists().should.be.ok; + }); + }); }; diff --git a/mobile/noarch/example/011_query_enumerator.js b/mobile/noarch/example/011_query_enumerator.js index b73e594..dfc9e99 100644 --- a/mobile/noarch/example/011_query_enumerator.js +++ b/mobile/noarch/example/011_query_enumerator.js @@ -109,6 +109,6 @@ module.exports = function() { e.reset(); var r3 = e.next(); r3.key.should.eql(0); - }) + }); }); }; \ No newline at end of file diff --git a/mobile/noarch/example/013_replication.js b/mobile/noarch/example/013_replication.js index bee46e9..e0a2408 100644 --- a/mobile/noarch/example/013_replication.js +++ b/mobile/noarch/example/013_replication.js @@ -90,15 +90,15 @@ module.exports = function() { it.skip('must have a addChangeListener method', function() { should(repl.addChangeListener).be.a.Function; - }) + }); it.skip('must have a removeChangeListener method', function() { should(repl.removeChangeListener).be.a.Function; - }) + }); it('must have a restart method', function() { should(repl.restart).be.a.Function; - }) + }); it('must have a setCredential method', function() { should(repl.setCredential).be.a.Function; @@ -106,11 +106,11 @@ module.exports = function() { it('must have a start method', function() { should(repl.start).be.a.Function; - }) + }); it('must have a stop method', function() { should(repl.stop).be.a.Function; - }) + }); }); @@ -126,9 +126,11 @@ module.exports = function() { it('must replicate an entire db', function(done) { this.timeout(10000); var db = manager.getDatabase('repl1'); + var hasStopped = false; repl = db.createPullReplication('http://'+conf.host+':'+conf.port+'/'+conf.dbname); - repl.addEventListener('status', function(e) { - if (e.status == titouchdb.REPLICATION_MODE_STOPPED) { + repl.addEventListener('change', function(e) { + if (!hasStopped && e.source.status == titouchdb.REPLICATION_MODE_STOPPED) { + hasStopped = true; should.not.exist(repl.lastError); db.documentCount.should.eql(118); repl.isRunning.should.eql(false); @@ -152,10 +154,12 @@ module.exports = function() { it('must replicate an entire db', function(done) { this.timeout(10000); var dbname = "repl2_" + Ti.Platform.createUUID().substring(0, 8).toLowerCase(); + var hasStopped = false; repl = db.createPushReplication('http://'+conf.host+':'+conf.port+'/'+dbname); repl.createTarget = true; - repl.addEventListener('status', function(e) { - if (e.status == titouchdb.REPLICATION_MODE_STOPPED) { + repl.addEventListener('change', function(e) { + if (!hasStopped && e.source.status == titouchdb.REPLICATION_MODE_STOPPED) { + hasStopped = true; should.not.exist(repl.lastError); repl.completedChangesCount.should.eql(12); repl.isRunning.should.eql(false); @@ -175,13 +179,43 @@ module.exports = function() { }); // currently returning a 400 error due to a request for /elements/_session - it.skip('must replicate with credentials', function(done) { + it('must replicate with credentials', function(done) { this.timeout(10000); var db = manager.getDatabase('repl3'); + var hasStopped = false; repl = db.createPullReplication('http://'+conf.host+':'+conf.port+'/'+conf.dbname); repl.setCredential({ user: 'scott', pass: 'tiger' }); - repl.addEventListener('status', function(e) { - if (e.status == titouchdb.REPLICATION_MODE_STOPPED) { + repl.addEventListener('change', function(e) { + if (!hasStopped && e.source.status == titouchdb.REPLICATION_MODE_STOPPED) { + hasStopped = true; + should.not.exist(repl.lastError); + db.documentCount.should.eql(118); + repl.isRunning.should.eql(false); + done(); + } + }); + repl.start(); + }); + }); + + describe('pull replication with authenticator', function() { + var conf, repl; + + before(function(done) { + utils.delete_nonsystem_databases(manager); + conf = utils.verify_couchdb_server('replication_config.json', done); + }); + + // currently returning a 400 error due to a request for /elements/_session + it('must replicate with a basic authenticator', function(done) { + this.timeout(10000); + var db = manager.getDatabase('repl4'); + var hasStopped = false; + repl = db.createPullReplication('http://'+conf.host+':'+conf.port+'/'+conf.dbname); + repl.authenticator = titouchdb.createBasicAuthenticator('scott', 'tiger'); + repl.addEventListener('change', function(e) { + if (!hasStopped && e.source.status == titouchdb.REPLICATION_MODE_STOPPED) { + hasStopped = true; should.not.exist(repl.lastError); db.documentCount.should.eql(118); repl.isRunning.should.eql(false); diff --git a/mobile/noarch/example/014_filtered_replication.js b/mobile/noarch/example/014_filtered_replication.js new file mode 100644 index 0000000..549c829 --- /dev/null +++ b/mobile/noarch/example/014_filtered_replication.js @@ -0,0 +1,60 @@ +require('ti-mocha'); + +var should = require('should'); +var utils = require('test_utils'); + +module.exports = function() { + var titouchdb = require('com.obscure.titouchdb'), + manager = titouchdb.databaseManager; + + describe('filtered replication (push)', function() { + var conf, repl, db; + + before(function(done) { + utils.delete_nonsystem_databases(manager); + db = utils.install_elements_database(manager); + conf = utils.verify_couchdb_server('replication_config.json', done); + }); + + it('must push with a filter', function(done) { + this.timeout(10000); + + var target_url = 'http://'+conf.host+':'+conf.port+'/noble_gases'; + + db.setFilter('noble_gases', function(doc, req) { + return [2, 10, 18, 36, 54, 86].indexOf(doc.atomic_number) != -1; + }); + + repl = db.createPushReplication(target_url); + repl.filter = 'noble_gases'; + repl.createTarget = true; + + var hasStopped = false; + repl.addEventListener('change', function(e) { + if (!hasStopped && e.source.status == titouchdb.REPLICATION_MODE_STOPPED) { + hasStopped = true; + should.not.exist(repl.lastError); + repl.isRunning.should.eql(false); + + var client = Ti.Network.createHTTPClient({ + onload: function(e) { + var resp = JSON.parse(this.responseText); + should.exist(resp); + should.exist(resp.doc_count); + resp.doc_count.should.eql(6); + done(); + }, + onerror: function(e) { + throw e; + } + }); + client.open("GET", target_url); + client.send(); + } + }); + repl.start(); + }); + + }); + +}; \ No newline at end of file diff --git a/mobile/noarch/example/app.js b/mobile/noarch/example/app.js index 687260f..afea2ff 100644 --- a/mobile/noarch/example/app.js +++ b/mobile/noarch/example/app.js @@ -18,6 +18,7 @@ require('010_queries')(); require('011_query_enumerator')(); require('012_query_row')(); require('013_replication')(); +require('014_filtered_replication')(); // create a window and run the tests var window = Ti.UI.createWindow({ diff --git a/samples/CannedDatabase/Resources/app.js b/samples/CannedDatabase/Resources/app.js index 956a084..fcf2592 100644 --- a/samples/CannedDatabase/Resources/app.js +++ b/samples/CannedDatabase/Resources/app.js @@ -11,14 +11,14 @@ var TiTouchDB = require('com.obscure.titouchdb'); * technique would not be suitable for large datasets. */ function generateDB() { - var db = TiTouchDB.databaseManager.createDatabaseNamed('elements'); + var db = TiTouchDB.databaseManager.getDatabase('elements'); var elements = [[1, "Hydrogen", "H"], [2, "Helium", "He"], [3, "Lithium", "Li"], [4, "Beryllium", "Be"], [5, "Boron", "B"], [6, "Carbon", "C"], [7, "Nitrogen", "N"], [8, "Oxygen", "O"], [9, "Fluorine", "F"], [10, "Neon", "Ne"], [11, "Sodium", "Na"], [12, "Magnesium", "Mg"], [13, "Aluminium", "Al", "http://0.tqn.com/d/chemistry/1/6/q/V/1/Aluminium.jpg"], [14, "Silicon", "Si"], [15, "Phosphorus", "P"], [16, "Sulfur", "S"], [17, "Chlorine", "Cl"], [18, "Argon", "Ar"], [19, "Potassium", "K"], [20, "Calcium", "Ca"], [21, "Scandium", "Sc"], [22, "Titanium", "Ti"], [23, "Vanadium", "V"], [24, "Chromium", "Cr"], [25, "Manganese", "Mn"], [26, "Iron", "Fe"], [27, "Cobalt", "Co", "http://0.tqn.com/d/chemistry/1/6/I/Z/1/cobalt.jpg"], [28, "Nickel", "Ni"], [29, "Copper", "Cu"], [30, "Zinc", "Zn"], [31, "Gallium", "Ga", "http://0.tqn.com/d/chemistry/1/6/H/Q/gallium.jpg"], [32, "Germanium", "Ge"], [33, "Arsenic", "As"], [34, "Selenium", "Se"], [35, "Bromine", "Br"], [36, "Krypton", "Kr"], [37, "Rubidium", "Rb"], [38, "Strontium", "Sr"], [39, "Yttrium", "Y"], [40, "Zirconium", "Zr"], [41, "Niobium", "Nb"], [42, "Molybdenum", "Mo"], [43, "Technetium", "Tc"], [44, "Ruthenium", "Ru"], [45, "Rhodium", "Rh"], [46, "Palladium", "Pd"], [47, "Silver", "Ag"], [48, "Cadmium", "Cd"], [49, "Indium", "In"], [50, "Tin", "Sn"], [51, "Antimony", "Sb"], [52, "Tellurium", "Te"], [53, "Iodine", "I"], [54, "Xenon", "Xe"], [55, "Caesium", "Cs"], [56, "Barium", "Ba"], [71, "Lutetium", "Lu"], [72, "Hafnium", "Hf"], [73, "Tantalum", "Ta"], [74, "Tungsten", "W"], [75, "Rhenium", "Re"], [76, "Osmium", "Os"], [77, "Iridium", "Ir"], [78, "Platinum", "Pt"], [79, "Gold", "Au"], [80, "Mercury", "Hg"], [81, "Thallium", "Tl"], [82, "Lead", "Pb"], [83, "Bismuth", "Bi", "http://0.tqn.com/d/chemistry/1/6/m/Q/bismuth.jpg"], [84, "Polonium", "Po"], [85, "Astatine", "At"], [86, "Radon", "Rn"], [87, "Francium", "Fr"], [88, "Radium", "Ra"], [103, "Lawrencium", "Lr"], [104, "Rutherfordium", "Rf"], [105, "Dubnium", "Db"], [106, "Seaborgium", "Sg"], [107, "Bohrium", "Bh"], [108, "Hassium", "Hs"], [109, "Meitnerium", "Mt"], [110, "Darmstadtium", "Ds"], [111, "Roentgenium", "Rg"], [112, "Copernicium", "Cn"], [113, "Ununtrium", "Uut"], [114, "Ununquadium", "Uuq"], [115, "Ununpentium", "Uup"], [116, "Ununhexium", "Uuh"], [117, "Ununseptium", "Uus"], [118, "Ununoctium", "Uuo"], [57, "Lanthanum", "La"], [58, "Cerium", "Ce"], [59, "Praseodymium", "Pr"], [60, "Neodymium", "Nd"], [61, "Promethium", "Pm"], [62, "Samarium", "Sm"], [63, "Europium", "Eu"], [64, "Gadolinium", "Gd"], [65, "Terbium", "Tb"], [66, "Dysprosium", "Dy"], [67, "Holmium", "Ho"], [68, "Erbium", "Er"], [69, "Thulium", "Tm"], [70, "Ytterbium", "Yb"], [89, "Actinium", "Ac"], [90, "Thorium", "Th"], [91, "Protactinium", "Pa"], [92, "Uranium", "U"], [93, "Neptunium", "Np"], [94, "Plutonium", "Pu"], [95, "Americium", "Am"], [96, "Curium", "Cm"], [97, "Berkelium", "Bk"], [98, "Californium", "Cf"], [99, "Einsteinium", "Es"], [100, "Fermium", "Fm"], [101, "Mendelevium", "Md"], [102, "Nobelium", "No"]]; function saveAttachment(doc, url) { - ++attcount'' + ++attcount; var client = Ti.Network.createHTTPClient({ onload: function(e) { - var rev = doc.newRevision(); + var rev = doc.createRevision(); rev.addAttachment('image.jpg', 'image/jpeg', client.responseData); rev.save(); } @@ -30,7 +30,7 @@ function generateDB() { var attcount = 0; for (i=0; i < elements.length; i++) { var e = elements[i]; - var doc = db.untitledDocument(); + var doc = db.createDocument(); doc.putProperties({ type: 'element', atomic_number: e[0], @@ -107,6 +107,15 @@ function copyDatabaseFiles() { copydir(srcdir, destdir); } +function installDatabase() { + var basedir = Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, 'assets', 'CouchbaseLite').path; + var dbfile = [basedir, 'elements.cblite'].join(Ti.Filesystem.separator); + var attdir = [basedir, 'elements attachments'].join(Ti.Filesystem.separator); + if (!TiTouchDB.databaseManager.replaceDatabase('elements', dbfile, attdir)) { + throw 'could not install elements database'; + } +} + var win = Ti.UI.createWindow({ backgroundColor: 'white' }); @@ -116,22 +125,22 @@ var tableView = Ti.UI.createTableView({ win.add(tableView); win.addEventListener('open', function(e) { - var db = TiTouchDB.databaseManager.databaseNamed('elements'); + var db = TiTouchDB.databaseManager.getExistingDatabase('elements'); if (!db) { - copyDatabaseFiles(); - db = TiTouchDB.databaseManager.databaseNamed('elements'); + installDatabase(); + db = TiTouchDB.databaseManager.getExistingDatabase('elements'); if (!db) { - alert("Error copying database files!"); + alert("Error creating database!"); return; } Ti.API.info("copied database files"); } - var query = db.queryAllDocuments(); + var query = db.createAllDocumentsQuery(); query.prefetch = true; - var rows = query.rows(); + var rows = query.run(); var data = []; - while (row = rows.nextRow()) { + while (row = rows.next()) { var props = row.documentProperties; data.push({ title: String.format("%s (%s)", props.name, props.symbol) diff --git a/samples/CannedDatabase/Resources/assets/CouchbaseLite/elements.touchdb b/samples/CannedDatabase/Resources/assets/CouchbaseLite/elements.cblite similarity index 100% rename from samples/CannedDatabase/Resources/assets/CouchbaseLite/elements.touchdb rename to samples/CannedDatabase/Resources/assets/CouchbaseLite/elements.cblite diff --git a/samples/CannedDatabase/Resources/assets/CouchbaseLite/elements.touchdb-shm b/samples/CannedDatabase/Resources/assets/CouchbaseLite/elements.touchdb-shm deleted file mode 100644 index fe9ac28..0000000 Binary files a/samples/CannedDatabase/Resources/assets/CouchbaseLite/elements.touchdb-shm and /dev/null differ diff --git a/samples/CannedDatabase/Resources/assets/CouchbaseLite/elements.touchdb-wal b/samples/CannedDatabase/Resources/assets/CouchbaseLite/elements.touchdb-wal deleted file mode 100644 index e69de29..0000000 diff --git a/samples/CannedDatabase/tiapp.xml b/samples/CannedDatabase/tiapp.xml index 2a73f82..c1d7fdb 100644 --- a/samples/CannedDatabase/tiapp.xml +++ b/samples/CannedDatabase/tiapp.xml @@ -38,14 +38,15 @@ default - com.obscure.titouchdb + com.obscure.titouchdb + false false true true true false - 3.0.2.GA + 3.2.3.GA diff --git a/samples/Migration/.gitignore b/samples/Migration/.gitignore new file mode 100644 index 0000000..71ac564 --- /dev/null +++ b/samples/Migration/.gitignore @@ -0,0 +1,8 @@ +.* +Resources +build.log +modules/android/com.obscure.titouchdb +modules/iphone/com.obscure.titouchdb +plugins/ti.alloy +app/config.json +app/assets/alloy diff --git a/samples/Migration/LICENSE.txt b/samples/Migration/LICENSE.txt new file mode 100644 index 0000000..388d0f5 --- /dev/null +++ b/samples/Migration/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright 2014 Paul Mietz Egli + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/samples/Migration/README.md b/samples/Migration/README.md new file mode 100644 index 0000000..cf2216b --- /dev/null +++ b/samples/Migration/README.md @@ -0,0 +1,7 @@ +# Migration sample project + +This project shows how to use the migrations feature of the TiTouchDB sync +adapter for Alloy. + +See also: +* [Docs for SQL migration feature](http://docs.appcelerator.com/titanium/3.0/#!/guide/Alloy_Sync_Adapters_and_Migrations-section-36739597_AlloySyncAdaptersandMigrations-Migrations) diff --git a/samples/Migration/app/alloy.js b/samples/Migration/app/alloy.js new file mode 100644 index 0000000..a439f3b --- /dev/null +++ b/samples/Migration/app/alloy.js @@ -0,0 +1,11 @@ +// The contents of this file will be executed before any of +// your view controllers are ever executed, including the index. +// You have access to all functionality on the `Alloy` namespace. +// +// This is a great place to do any initialization for your app +// or create any global variables/functions that you'd like to +// make available throughout your app. You can easily make things +// accessible globally by attaching them to the `Alloy.Globals` +// object. For example: +// +// Alloy.Globals.someGlobalFunction = function(){}; diff --git a/samples/Migration/app/assets/android/MarketplaceArtwork.png b/samples/Migration/app/assets/android/MarketplaceArtwork.png new file mode 100644 index 0000000..fffab1b Binary files /dev/null and b/samples/Migration/app/assets/android/MarketplaceArtwork.png differ diff --git a/samples/Migration/app/assets/android/appicon.png b/samples/Migration/app/assets/android/appicon.png new file mode 100644 index 0000000..7e73d18 Binary files /dev/null and b/samples/Migration/app/assets/android/appicon.png differ diff --git a/samples/Migration/app/assets/android/default.png b/samples/Migration/app/assets/android/default.png new file mode 100644 index 0000000..2578dc1 Binary files /dev/null and b/samples/Migration/app/assets/android/default.png differ diff --git a/samples/Migration/app/assets/android/images/res-long-land-hdpi/default.png b/samples/Migration/app/assets/android/images/res-long-land-hdpi/default.png new file mode 100644 index 0000000..289320d Binary files /dev/null and b/samples/Migration/app/assets/android/images/res-long-land-hdpi/default.png differ diff --git a/samples/Migration/app/assets/android/images/res-long-land-ldpi/default.png b/samples/Migration/app/assets/android/images/res-long-land-ldpi/default.png new file mode 100644 index 0000000..ed0cbf3 Binary files /dev/null and b/samples/Migration/app/assets/android/images/res-long-land-ldpi/default.png differ diff --git a/samples/Migration/app/assets/android/images/res-long-port-hdpi/default.png b/samples/Migration/app/assets/android/images/res-long-port-hdpi/default.png new file mode 100644 index 0000000..15dd8a7 Binary files /dev/null and b/samples/Migration/app/assets/android/images/res-long-port-hdpi/default.png differ diff --git a/samples/Migration/app/assets/android/images/res-long-port-ldpi/default.png b/samples/Migration/app/assets/android/images/res-long-port-ldpi/default.png new file mode 100644 index 0000000..f472001 Binary files /dev/null and b/samples/Migration/app/assets/android/images/res-long-port-ldpi/default.png differ diff --git a/samples/Migration/app/assets/android/images/res-notlong-land-hdpi/default.png b/samples/Migration/app/assets/android/images/res-notlong-land-hdpi/default.png new file mode 100644 index 0000000..289320d Binary files /dev/null and b/samples/Migration/app/assets/android/images/res-notlong-land-hdpi/default.png differ diff --git a/samples/Migration/app/assets/android/images/res-notlong-land-ldpi/default.png b/samples/Migration/app/assets/android/images/res-notlong-land-ldpi/default.png new file mode 100644 index 0000000..6cdb7d0 Binary files /dev/null and b/samples/Migration/app/assets/android/images/res-notlong-land-ldpi/default.png differ diff --git a/samples/Migration/app/assets/android/images/res-notlong-land-mdpi/default.png b/samples/Migration/app/assets/android/images/res-notlong-land-mdpi/default.png new file mode 100644 index 0000000..ff7e57d Binary files /dev/null and b/samples/Migration/app/assets/android/images/res-notlong-land-mdpi/default.png differ diff --git a/samples/Migration/app/assets/android/images/res-notlong-port-hdpi/default.png b/samples/Migration/app/assets/android/images/res-notlong-port-hdpi/default.png new file mode 100644 index 0000000..15dd8a7 Binary files /dev/null and b/samples/Migration/app/assets/android/images/res-notlong-port-hdpi/default.png differ diff --git a/samples/Migration/app/assets/android/images/res-notlong-port-ldpi/default.png b/samples/Migration/app/assets/android/images/res-notlong-port-ldpi/default.png new file mode 100644 index 0000000..06d0921 Binary files /dev/null and b/samples/Migration/app/assets/android/images/res-notlong-port-ldpi/default.png differ diff --git a/samples/Migration/app/assets/android/images/res-notlong-port-mdpi/default.png b/samples/Migration/app/assets/android/images/res-notlong-port-mdpi/default.png new file mode 100644 index 0000000..2578dc1 Binary files /dev/null and b/samples/Migration/app/assets/android/images/res-notlong-port-mdpi/default.png differ diff --git a/samples/Migration/app/assets/iphone/Default-568h@2x.png b/samples/Migration/app/assets/iphone/Default-568h@2x.png new file mode 100644 index 0000000..e525fdb Binary files /dev/null and b/samples/Migration/app/assets/iphone/Default-568h@2x.png differ diff --git a/samples/Migration/app/assets/iphone/Default-Landscape.png b/samples/Migration/app/assets/iphone/Default-Landscape.png new file mode 100644 index 0000000..45bcaa2 Binary files /dev/null and b/samples/Migration/app/assets/iphone/Default-Landscape.png differ diff --git a/samples/Migration/app/assets/iphone/Default-Landscape@2x.png b/samples/Migration/app/assets/iphone/Default-Landscape@2x.png new file mode 100644 index 0000000..4fd45d0 Binary files /dev/null and b/samples/Migration/app/assets/iphone/Default-Landscape@2x.png differ diff --git a/samples/Migration/app/assets/iphone/Default-Portrait.png b/samples/Migration/app/assets/iphone/Default-Portrait.png new file mode 100644 index 0000000..67996fd Binary files /dev/null and b/samples/Migration/app/assets/iphone/Default-Portrait.png differ diff --git a/samples/Migration/app/assets/iphone/Default-Portrait@2x.png b/samples/Migration/app/assets/iphone/Default-Portrait@2x.png new file mode 100644 index 0000000..c67b596 Binary files /dev/null and b/samples/Migration/app/assets/iphone/Default-Portrait@2x.png differ diff --git a/samples/Migration/app/assets/iphone/Default.png b/samples/Migration/app/assets/iphone/Default.png new file mode 100644 index 0000000..6ad4820 Binary files /dev/null and b/samples/Migration/app/assets/iphone/Default.png differ diff --git a/samples/Migration/app/assets/iphone/Default@2x.png b/samples/Migration/app/assets/iphone/Default@2x.png new file mode 100644 index 0000000..62add74 Binary files /dev/null and b/samples/Migration/app/assets/iphone/Default@2x.png differ diff --git a/samples/Migration/app/assets/iphone/appicon-72.png b/samples/Migration/app/assets/iphone/appicon-72.png new file mode 100644 index 0000000..8fdf5a9 Binary files /dev/null and b/samples/Migration/app/assets/iphone/appicon-72.png differ diff --git a/samples/Migration/app/assets/iphone/appicon-72@2x.png b/samples/Migration/app/assets/iphone/appicon-72@2x.png new file mode 100644 index 0000000..b3ef1ca Binary files /dev/null and b/samples/Migration/app/assets/iphone/appicon-72@2x.png differ diff --git a/samples/Migration/app/assets/iphone/appicon-Small-50.png b/samples/Migration/app/assets/iphone/appicon-Small-50.png new file mode 100644 index 0000000..244b8ec Binary files /dev/null and b/samples/Migration/app/assets/iphone/appicon-Small-50.png differ diff --git a/samples/Migration/app/assets/iphone/appicon-Small.png b/samples/Migration/app/assets/iphone/appicon-Small.png new file mode 100644 index 0000000..f74c286 Binary files /dev/null and b/samples/Migration/app/assets/iphone/appicon-Small.png differ diff --git a/samples/Migration/app/assets/iphone/appicon-Small@2x.png b/samples/Migration/app/assets/iphone/appicon-Small@2x.png new file mode 100644 index 0000000..b94865e Binary files /dev/null and b/samples/Migration/app/assets/iphone/appicon-Small@2x.png differ diff --git a/samples/Migration/app/assets/iphone/appicon.png b/samples/Migration/app/assets/iphone/appicon.png new file mode 100644 index 0000000..f7bde5b Binary files /dev/null and b/samples/Migration/app/assets/iphone/appicon.png differ diff --git a/samples/Migration/app/assets/iphone/appicon@2x.png b/samples/Migration/app/assets/iphone/appicon@2x.png new file mode 100644 index 0000000..05ee350 Binary files /dev/null and b/samples/Migration/app/assets/iphone/appicon@2x.png differ diff --git a/samples/Migration/app/assets/iphone/iTunesArtwork b/samples/Migration/app/assets/iphone/iTunesArtwork new file mode 100644 index 0000000..a98a73a Binary files /dev/null and b/samples/Migration/app/assets/iphone/iTunesArtwork differ diff --git a/samples/Migration/app/config.json b/samples/Migration/app/config.json new file mode 100644 index 0000000..07c1f1c --- /dev/null +++ b/samples/Migration/app/config.json @@ -0,0 +1,11 @@ +{ + "global": {}, + "env:development": {}, + "env:test": {}, + "env:production": {}, + "os:android": {}, + "os:blackberry": {}, + "os:ios": {}, + "os:mobileweb": {}, + "dependencies": {} +} \ No newline at end of file diff --git a/samples/Migration/app/controllers/index.js b/samples/Migration/app/controllers/index.js new file mode 100644 index 0000000..6dd9a2d --- /dev/null +++ b/samples/Migration/app/controllers/index.js @@ -0,0 +1,13 @@ + +function window_open(e) { + Alloy.Collections.contact.fetch(); +} + +function transform(model) { + var result = model.toJSON(); + result.full_name = String.format("%s, %s", result.last, result.first); + Ti.API.info(JSON.stringify(result)); + return result; +} + +$.index.open(); diff --git a/samples/Migration/app/migrations/201405140800_Contact.js b/samples/Migration/app/migrations/201405140800_Contact.js new file mode 100644 index 0000000..a56bb66 --- /dev/null +++ b/samples/Migration/app/migrations/201405140800_Contact.js @@ -0,0 +1,37 @@ +// http://www.generatedata.com +var preload_data = [ + { "last": "Hall", "first": "Xena", "street": "5444 Cras Rd.", "city": "Carapicuíba", "state": "São Paulo" }, + { "last": "Grant", "first": "Macaulay", "street": "462 Curae; Av.", "city": "Racine", "state": "WI" }, + { "last": "Perez", "first": "Orlando", "street": "P.O. Box 588", "city": "Ellesmere", "state": "OH" }, + { "last": "Tucker", "first": "Wayne", "street": "386-1375 Lorem Ave", "city": "Fresno", "state": "CA" }, + { "last": "Owen", "first": "Fuller", "street": "889 Nulla Ave", "city": "Lincoln", "state": "NB" }, + { "last": "Jimenez", "first": "Blair", "street": "4732 Turpis. St.", "city": "Campbelltown", "state": "GA" }, + { "last": "Maxwell", "first": "Ashton", "street": "118 Pellentesque Av. Apt 4", "city": "Knoxville", "state": "TN" }, + { "last": "Glover", "first": "Candice", "street": "7281 Integer Rd.", "city": "Vienna", "state": "TX" }, + { "last": "Pace", "first": "Joan", "street": "3455 Sodales Rd.", "city": "Bellary", "state": "KY" }, + { "last": "Carney", "first": "Lacota", "street": "5413 Elementum Avenue", "city": "Whitehorse", "state": "YT" } +]; + +migration.up = function(migrator) { + var db = migrator.database; + _.each(preload_data, function(contact) { + migrator.createModel(contact); + }); +}; + +migration.down = function(migrator) { + var db = migrator.database; + var q = db.createSlowQuery(function(doc) { + emit([doc.last, doc.first], null); + }); + var e = q.run(); + while (row = e.next()) { + var key = row.key; + if (_.find(preload_data, function(contact) { + return contact.last === key[0] && contact.first === key[1]; + })) { + row.getDocument().deleteDocument(); + } + } + +}; diff --git a/samples/Migration/app/migrations/201406110800_Contact.js b/samples/Migration/app/migrations/201406110800_Contact.js new file mode 100644 index 0000000..98e9163 --- /dev/null +++ b/samples/Migration/app/migrations/201406110800_Contact.js @@ -0,0 +1,35 @@ +// http://www.generatedata.com +var preload_data = [ + { "first": "Xaviera", "last": "Stafford", "street": "449 Lobortis St.", "city": "Missoula", "state": "MT" }, + { "first": "Alexander", "last": "Strickland", "street": "2851 Consequat Av.", "city": "Fuenlabrada", "state": "MA" }, + { "first": "Oren", "last": "Fletcher", "street": "Ap #192-4259 Velit Avenue", "city": "Kraków", "state": "MP" }, + { "first": "Rachel", "last": "Fowler", "street": "6764 Sit Rd.", "city": "Raymond", "state": "AB" }, + { "first": "Yoko", "last": "Stewart", "street": "62022 Ornare Road", "city": "Providence", "state": "RI" }, + { "first": "Brent", "last": "Winters", "street": "6075 Donec St.", "city": "Atwater", "state": "CA" } +]; + +migration.up = function(migrator) { + var db = migrator.database; + _.each(preload_data, function(contact) { + migrator.createModel(contact); + }); +}; + +migration.down = function(migrator) { + Ti.API.info("down"); + var db = migrator.database; + var q = db.createSlowQuery(function(doc) { + emit([doc.last, doc.first], null); + }); + var e = q.run(); + while (row = e.next()) { + var key = row.key; + if (_.find(preload_data, function(contact) { + return contact.last === key[0] && contact.first === key[1]; + })) { + row.getDocument().deleteDocument(); + } + } + +}; + diff --git a/samples/Migration/app/models/Contact.js b/samples/Migration/app/models/Contact.js new file mode 100644 index 0000000..a94465e --- /dev/null +++ b/samples/Migration/app/models/Contact.js @@ -0,0 +1,48 @@ +/* + * To test down-migrations, run the app once to get both migration datasets + * inserted, then uncomment the "migration" property below and run again. The + * adapter should remove the data added in the 201406110800 migration file. + */ + +exports.definition = { + + config: { + adapter: { + type: "titouchdb", + dbname: "contacts", + // migration: "201405140800", + views: [ + { + name: "by_lastname", + version: '1', + map: function(doc) { + if (doc.modelname == 'contact' && doc.last) { + emit(doc.last, null); + } + } + } + ], + view_options: { + prefetch: true + }, + modelname: 'contact' + } + }, + + extendModel: function(Model) { + _.extend(Model.prototype, { + }); + return Model; + }, + + extendCollection: function(Collection) { + _.extend(Collection.prototype, { + map_row: function(Model, row) { + var result = new Model(row.documentProperties); + // add custom properties here, if any + return result; + } + }); + return Collection; + } +}; diff --git a/samples/Migration/app/styles/index.tss b/samples/Migration/app/styles/index.tss new file mode 100644 index 0000000..174db0b --- /dev/null +++ b/samples/Migration/app/styles/index.tss @@ -0,0 +1,4 @@ +"Window": { + backgroundColor:"white" +}, + diff --git a/samples/Migration/app/views/index.xml b/samples/Migration/app/views/index.xml new file mode 100644 index 0000000..87d03ef --- /dev/null +++ b/samples/Migration/app/views/index.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/Migration/manifest b/samples/Migration/manifest new file mode 100644 index 0000000..ddd14fd --- /dev/null +++ b/samples/Migration/manifest @@ -0,0 +1,8 @@ +#appname:Migration +#publisher:Paul Mietz Egli +#url:https://github.com/pegli/ti_touchdb +#image:appicon.png +#appid:com.obscure.migration +#desc:not specified +#type:mobile +#guid:9140e913-ec8e-4f9c-8f47-346a0e8cbfc3 diff --git a/samples/Migration/tiapp.xml b/samples/Migration/tiapp.xml new file mode 100644 index 0000000..82e5e0c --- /dev/null +++ b/samples/Migration/tiapp.xml @@ -0,0 +1,65 @@ + + + com.obscure.migration + Migration + 1.0 + Paul Mietz Egli + https://github.com/pegli/ti_touchdb + not specified + 2014 by Paul Mietz Egli + appicon.png + false + false + true + 9140e913-ec8e-4f9c-8f47-346a0e8cbfc3 + dp + + + + UISupportedInterfaceOrientations~iphone + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIRequiresPersistentWiFi + + UIPrerenderedIcon + + UIStatusBarHidden + + UIStatusBarStyle + UIStatusBarStyleDefault + + + + + + + + true + true + + default + + + com.obscure.titouchdb + + + true + false + false + true + false + false + + 3.3.0.GA + + ti.alloy + + diff --git a/samples/ToDoLite/.gitignore b/samples/ToDoLite/.gitignore new file mode 100644 index 0000000..7536cba --- /dev/null +++ b/samples/ToDoLite/.gitignore @@ -0,0 +1,7 @@ +build.log +build +npm-debug.log +tmp +modules +Resources +.* diff --git a/samples/ToDoLite/LICENSE.txt b/samples/ToDoLite/LICENSE.txt new file mode 100644 index 0000000..1374352 --- /dev/null +++ b/samples/ToDoLite/LICENSE.txt @@ -0,0 +1,13 @@ + Copyright 2014 Paul Mietz Egli + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/samples/ToDoLite/README.md b/samples/ToDoLite/README.md new file mode 100644 index 0000000..04930ca --- /dev/null +++ b/samples/ToDoLite/README.md @@ -0,0 +1,25 @@ +# ToDoLite Sample App + +This sample app is designed to be a functional equivalent of the [ToDoLite](https://github.com/couchbaselabs/ToDoLite-iOS) +example provided by Couchbase. + +## Setup + +1. Install the TiTouchDB module using gittio or by copying the module ZIP files to this directory. +1. Create a directory named `app/assets/alloy/sync` and copy the `titouchdb.js` sync adapter file + to that directory. If you have cloned this project from Github, you can make a symlink by + running `ln -s ../../../../mobile/noarch/alloy app/assets/`. +1. TODO set up remote database to sync to. +1. Build and run as usual. + +## Known Issues + +TODO list differences between Couchbase ToDoLite and this app + +## FAQ + +*Why are the event handler names so weird? It's almost like I'm reading Objective-C!* + +I named views, methods, and classes based on their correspondance with the original ToDoLite +application. For example, `detail.js` contains an event handler named `shareButtonAction`, +which performs the same function as `shareButtonAction:` in `DetailViewController`. diff --git a/samples/ToDoLite/app/alloy.js b/samples/ToDoLite/app/alloy.js new file mode 100644 index 0000000..21e9775 --- /dev/null +++ b/samples/ToDoLite/app/alloy.js @@ -0,0 +1,54 @@ + + +(function() { + var sync = require('lib/sync'); + var cblSync; + + /* + * Configure sync and trigger it if the user is already logged in. + */ + + function updateMyLists(userID, userData) { + // create a new profile document + // TODO figure out a way to construct the _id value in the sync adapter + var profile = Alloy.createModel('profile', { name: userData.name, user_id: userID }); + profile.id = "p:" + userID; + Alloy.Collections.list.updateAllListsWithOwner(profile.id); + profile.save(); + } + + // public + Alloy.Globals.loginAndSync = function(cb) { + if (cblSync.userID) { + _.isFunction(cb) && cb(); + } + else { + cblSync.beforeFirstSync(cb); + cblSync.start(); + } + }; + + // run at startup to load up the sync manager for the database and remote URL + cblSync = sync.createSyncManager({ + database: Alloy.CFG.dbname, + url: Ti.App.Properties.getString('com.couchbase.todolite.syncurl', 'http://localhost:4984/todos'), + }); + + // set the authenticator on the sync manager + cblSync.setAuthenticator(sync.createFacebookAuthenticator({ + appid: Ti.App.Properties.getString('ti.facebook.appid') + })); + + if (cblSync.userID) { + cblSync.start(); + } + else { + cblSync.beforeFirstSync(function(userID, userData, err) { + updateMyLists(userID, userData); + }); + } + + // equivalent to adding a property to AppDelegate + Alloy.Globals.cblSync = cblSync; + +})(); diff --git a/samples/ToDoLite/app/assets/alloy b/samples/ToDoLite/app/assets/alloy new file mode 120000 index 0000000..5c3eb63 --- /dev/null +++ b/samples/ToDoLite/app/assets/alloy @@ -0,0 +1 @@ +../../../../mobile/noarch/alloy \ No newline at end of file diff --git a/samples/ToDoLite/app/assets/android/MarketplaceArtwork.png b/samples/ToDoLite/app/assets/android/MarketplaceArtwork.png new file mode 100644 index 0000000..fffab1b Binary files /dev/null and b/samples/ToDoLite/app/assets/android/MarketplaceArtwork.png differ diff --git a/samples/ToDoLite/app/assets/android/appicon.png b/samples/ToDoLite/app/assets/android/appicon.png new file mode 100644 index 0000000..7e73d18 Binary files /dev/null and b/samples/ToDoLite/app/assets/android/appicon.png differ diff --git a/samples/ToDoLite/app/assets/android/default.png b/samples/ToDoLite/app/assets/android/default.png new file mode 100644 index 0000000..2578dc1 Binary files /dev/null and b/samples/ToDoLite/app/assets/android/default.png differ diff --git a/samples/ToDoLite/app/assets/android/images/res-long-land-hdpi/default.png b/samples/ToDoLite/app/assets/android/images/res-long-land-hdpi/default.png new file mode 100644 index 0000000..289320d Binary files /dev/null and b/samples/ToDoLite/app/assets/android/images/res-long-land-hdpi/default.png differ diff --git a/samples/ToDoLite/app/assets/android/images/res-long-land-ldpi/default.png b/samples/ToDoLite/app/assets/android/images/res-long-land-ldpi/default.png new file mode 100644 index 0000000..ed0cbf3 Binary files /dev/null and b/samples/ToDoLite/app/assets/android/images/res-long-land-ldpi/default.png differ diff --git a/samples/ToDoLite/app/assets/android/images/res-long-port-hdpi/default.png b/samples/ToDoLite/app/assets/android/images/res-long-port-hdpi/default.png new file mode 100644 index 0000000..15dd8a7 Binary files /dev/null and b/samples/ToDoLite/app/assets/android/images/res-long-port-hdpi/default.png differ diff --git a/samples/ToDoLite/app/assets/android/images/res-long-port-ldpi/default.png b/samples/ToDoLite/app/assets/android/images/res-long-port-ldpi/default.png new file mode 100644 index 0000000..f472001 Binary files /dev/null and b/samples/ToDoLite/app/assets/android/images/res-long-port-ldpi/default.png differ diff --git a/samples/ToDoLite/app/assets/android/images/res-notlong-land-hdpi/default.png b/samples/ToDoLite/app/assets/android/images/res-notlong-land-hdpi/default.png new file mode 100644 index 0000000..289320d Binary files /dev/null and b/samples/ToDoLite/app/assets/android/images/res-notlong-land-hdpi/default.png differ diff --git a/samples/ToDoLite/app/assets/android/images/res-notlong-land-ldpi/default.png b/samples/ToDoLite/app/assets/android/images/res-notlong-land-ldpi/default.png new file mode 100644 index 0000000..6cdb7d0 Binary files /dev/null and b/samples/ToDoLite/app/assets/android/images/res-notlong-land-ldpi/default.png differ diff --git a/samples/ToDoLite/app/assets/android/images/res-notlong-land-mdpi/default.png b/samples/ToDoLite/app/assets/android/images/res-notlong-land-mdpi/default.png new file mode 100644 index 0000000..ff7e57d Binary files /dev/null and b/samples/ToDoLite/app/assets/android/images/res-notlong-land-mdpi/default.png differ diff --git a/samples/ToDoLite/app/assets/android/images/res-notlong-port-hdpi/default.png b/samples/ToDoLite/app/assets/android/images/res-notlong-port-hdpi/default.png new file mode 100644 index 0000000..15dd8a7 Binary files /dev/null and b/samples/ToDoLite/app/assets/android/images/res-notlong-port-hdpi/default.png differ diff --git a/samples/ToDoLite/app/assets/android/images/res-notlong-port-ldpi/default.png b/samples/ToDoLite/app/assets/android/images/res-notlong-port-ldpi/default.png new file mode 100644 index 0000000..06d0921 Binary files /dev/null and b/samples/ToDoLite/app/assets/android/images/res-notlong-port-ldpi/default.png differ diff --git a/samples/ToDoLite/app/assets/android/images/res-notlong-port-mdpi/default.png b/samples/ToDoLite/app/assets/android/images/res-notlong-port-mdpi/default.png new file mode 100644 index 0000000..2578dc1 Binary files /dev/null and b/samples/ToDoLite/app/assets/android/images/res-notlong-port-mdpi/default.png differ diff --git a/samples/ToDoLite/app/assets/images/Camera-Light@2x.png b/samples/ToDoLite/app/assets/images/Camera-Light@2x.png new file mode 100644 index 0000000..5d4b5c2 Binary files /dev/null and b/samples/ToDoLite/app/assets/images/Camera-Light@2x.png differ diff --git a/samples/ToDoLite/app/assets/images/Camera@2x.png b/samples/ToDoLite/app/assets/images/Camera@2x.png new file mode 100644 index 0000000..71d9958 Binary files /dev/null and b/samples/ToDoLite/app/assets/images/Camera@2x.png differ diff --git a/samples/ToDoLite/app/assets/images/README.md b/samples/ToDoLite/app/assets/images/README.md new file mode 100644 index 0000000..b9d0d73 --- /dev/null +++ b/samples/ToDoLite/app/assets/images/README.md @@ -0,0 +1,3 @@ +Some images copyright (c) 2011-2013 Couchbase, Inc. +Released under the Apache License 2.0 + diff --git a/samples/ToDoLite/app/assets/images/task_image_mask@2x.png b/samples/ToDoLite/app/assets/images/task_image_mask@2x.png new file mode 100644 index 0000000..8019a9b Binary files /dev/null and b/samples/ToDoLite/app/assets/images/task_image_mask@2x.png differ diff --git a/samples/ToDoLite/app/assets/iphone/Default-568h@2x.png b/samples/ToDoLite/app/assets/iphone/Default-568h@2x.png new file mode 100644 index 0000000..e525fdb Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/Default-568h@2x.png differ diff --git a/samples/ToDoLite/app/assets/iphone/Default-Landscape.png b/samples/ToDoLite/app/assets/iphone/Default-Landscape.png new file mode 100644 index 0000000..45bcaa2 Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/Default-Landscape.png differ diff --git a/samples/ToDoLite/app/assets/iphone/Default-Landscape@2x.png b/samples/ToDoLite/app/assets/iphone/Default-Landscape@2x.png new file mode 100644 index 0000000..4fd45d0 Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/Default-Landscape@2x.png differ diff --git a/samples/ToDoLite/app/assets/iphone/Default-Portrait.png b/samples/ToDoLite/app/assets/iphone/Default-Portrait.png new file mode 100644 index 0000000..67996fd Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/Default-Portrait.png differ diff --git a/samples/ToDoLite/app/assets/iphone/Default-Portrait@2x.png b/samples/ToDoLite/app/assets/iphone/Default-Portrait@2x.png new file mode 100644 index 0000000..c67b596 Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/Default-Portrait@2x.png differ diff --git a/samples/ToDoLite/app/assets/iphone/Default.png b/samples/ToDoLite/app/assets/iphone/Default.png new file mode 100644 index 0000000..6ad4820 Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/Default.png differ diff --git a/samples/ToDoLite/app/assets/iphone/Default@2x.png b/samples/ToDoLite/app/assets/iphone/Default@2x.png new file mode 100644 index 0000000..62add74 Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/Default@2x.png differ diff --git a/samples/ToDoLite/app/assets/iphone/appicon-72.png b/samples/ToDoLite/app/assets/iphone/appicon-72.png new file mode 100644 index 0000000..8fdf5a9 Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/appicon-72.png differ diff --git a/samples/ToDoLite/app/assets/iphone/appicon-72@2x.png b/samples/ToDoLite/app/assets/iphone/appicon-72@2x.png new file mode 100644 index 0000000..b3ef1ca Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/appicon-72@2x.png differ diff --git a/samples/ToDoLite/app/assets/iphone/appicon-Small-50.png b/samples/ToDoLite/app/assets/iphone/appicon-Small-50.png new file mode 100644 index 0000000..244b8ec Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/appicon-Small-50.png differ diff --git a/samples/ToDoLite/app/assets/iphone/appicon-Small.png b/samples/ToDoLite/app/assets/iphone/appicon-Small.png new file mode 100644 index 0000000..f74c286 Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/appicon-Small.png differ diff --git a/samples/ToDoLite/app/assets/iphone/appicon-Small@2x.png b/samples/ToDoLite/app/assets/iphone/appicon-Small@2x.png new file mode 100644 index 0000000..b94865e Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/appicon-Small@2x.png differ diff --git a/samples/ToDoLite/app/assets/iphone/appicon.png b/samples/ToDoLite/app/assets/iphone/appicon.png new file mode 100644 index 0000000..f7bde5b Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/appicon.png differ diff --git a/samples/ToDoLite/app/assets/iphone/appicon@2x.png b/samples/ToDoLite/app/assets/iphone/appicon@2x.png new file mode 100644 index 0000000..05ee350 Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/appicon@2x.png differ diff --git a/samples/ToDoLite/app/assets/iphone/iTunesArtwork b/samples/ToDoLite/app/assets/iphone/iTunesArtwork new file mode 100644 index 0000000..a98a73a Binary files /dev/null and b/samples/ToDoLite/app/assets/iphone/iTunesArtwork differ diff --git a/samples/ToDoLite/app/assets/lib/sync.js b/samples/ToDoLite/app/assets/lib/sync.js new file mode 100644 index 0000000..2adde9d --- /dev/null +++ b/samples/ToDoLite/app/assets/lib/sync.js @@ -0,0 +1,231 @@ +/** + * Synchronization manager module. + * + * This module keeps track of the push and pull replications for a + * database and holds references to any long-lived objects related + * to sync. + */ + +var titouchdb = require('com.obscure.titouchdb'), + manager = titouchdb.databaseManager; + +var fb = require('facebook'); + +// SYNC MANAGER + +function SyncManager(dbname, url, userID) { + this.database = manager.getDatabase(dbname); + this.userID = userID || Ti.App.Properties.getString('sync_manager.userid'); + this.replicationURL = url; + + // privileged methods + + this.defineSync = _defineSync; + this.setupNewUser = _setupNewUser; + this.launchSync = _launchSync; + this.runBeforeSyncStart = _runBeforeSyncStart; + this.replicationProgress = _replicationProgress; + + this.beforeFirstSync(function(uid, userData) { + Ti.App.Properties.setString('sync_manager.userid', uid); + Ti.API.info("stored userid "+uid); + }); +} + +// PUBLIC METHODS + +SyncManager.prototype.start = function() { + if (!this.userID) { + var self = this; + this.setupNewUser(function() { + self.launchSync(); + }); + } + else { + this.launchSync(); + } +}; + +SyncManager.prototype.restartSync = function() { + this.pull.stop(); + this.pull.start(); + this.push.stop(); + this.push.start(); + Ti.API.info("restartSync"); +}; + +SyncManager.prototype.beforeFirstSync = function(cb) { + this.beforeSyncBlocks = (this.beforeSyncBlocks || []).concat([cb]); +}; + +SyncManager.prototype.onSyncConnected = function(cb) { + this.onSyncStartedBlocks = (this.onSyncStartedBlocks || []).concat([cb]); +}; + +SyncManager.prototype.setAuthenticator = function(authenticator) { + this.authenticator = authenticator; + authenticator.setSyncManager(this); + if (this.lastAuthError) { + this.runAuthenticator(); + } +}; + +// PRIVATE METHODS + +function _setupNewUser(cb) { + if (this.userID) return; + + var self = this; + this.authenticator && this.authenticator.getCredentials(function(uid, userData) { + self.userID = uid; + var err = self.runBeforeSyncStart(uid, userData); + if (err) { + Ti.API.error(err); + } + else { + _.isFunction(cb) && cb(); + } + }); +} + +function _runBeforeSyncStart(uid, userData) { + var err; + _.each(this.beforeSyncBlocks, function(b) { + if (_.isFunction(b)) { + err = b(uid, userData); + } + if (err) return err; + }); + + return err; +} + +function _runAuthenticator() { + this.authenticator && this.authenticator.getCredentials(function(uid, userData) { + if (uid !== this.userID) { + throw("cannot change userID from " + this.userID + " to " + uid + "; need to reinstall"); + } + this.restartSync(); + }); +} + +function _launchSync() { + this.defineSync(); + if (this.lastAuthError) { + this.runAuthenticator(); + } + else { + this.restartSync(); + } +} + +function _defineSync() { + this.pull = this.database.createPullReplication(this.replicationURL); + this.pull.continuous = true; + this.pull.addEventListener('change', this.replicationProgress); + + this.push = this.database.createPushReplication(this.replicationURL); + this.push.continuous = true; + this.push.addEventListener('change', this.replicationProgress); + + this.authenticator.registerCredentialsWithReplications([this.pull, this.push]); +} + +function _replicationProgress(e) { + // this is run for pull and push independently + var active = false; + var completed = 0, total = 0; + var status = titouchdb.REPLICATION_MODE_STOPPED; + var error; + + var repl = e.source; + status = Math.max(status, repl.status); + if (!error) { + error = repl.lastError; + } + if (repl.status === titouchdb.REPLICATION_MODE_ACTIVE) { + active = true; + completed += repl.completedChangesCount; + total += repl.changesCount; + } + + if (error && error.code === 401) { + if (!this.authenticator) { + this.lastAuthError = error; + return; + } + this.runAuthenticator(); + } + + if (active !== this.active || completed !== this.completed || total !== this.total || error !== this.error) { + this.active = active; + this.completed = completed; + this.total = total; + this.error = error; + this.progress = (completed / Math.max(total, 1)); + + Ti.API.info(String.format("SyncManager: active=%s; status=%d; %d/%d; " + (error ? JSON.stringify(error) : ""), active ? "true" : "false", status, completed, total)); + + // fire an event to notify the app that the data may have changed + Ti.App.fireEvent('sync:change', {}); + } + +} + +// FACEBOOK AUTHENTICATOR + +function FacebookAuthenticator(appid) { + fb.appid = appid; + fb.permissions = ["public_profile", "user_friends", "email"]; + fb.forceDialogAuth = false; +} + +FacebookAuthenticator.prototype.setSyncManager = function(syncManager) { + this.syncManager = syncManager; +}; + + +FacebookAuthenticator.prototype.getCredentials = function(cb) { + // if the user is logged in, the Ti Facebook module skips the + // call to authorize(). In this case, we make a graph API call + // to get the user data. + if (fb.loggedIn) { + fb.requestWithGraphPath('/me', { fields: "id,name,email" }, 'GET', function(e) { + if (e.success) { + var data = JSON.parse(e.result); + _.isFunction(cb) && cb(data.email, data); + } + }); + } + else { + var f = function(e) { + if (e.success) { + _.isFunction(cb) && cb(e.data.email, e.data); + } + fb.removeEventListener('login', f); + }; + fb.addEventListener('login', f); + fb.authorize(); + } +}; + +FacebookAuthenticator.prototype.registerCredentialsWithReplications = function(repls) { + if (fb.loggedIn) { + _.each(repls, function(r) { + r.authenticator = titouchdb.createFacebookAuthenticator(fb.accessToken); + }); + } + else { + Ti.API.warn("could not set authenticators for replications: not logged in"); + } +}; + +// PUBLIC API + +exports.createSyncManager = function(opts) { + return new SyncManager(opts.database, opts.url, opts.user); +}; + +exports.createFacebookAuthenticator = function(opts) { + return new FacebookAuthenticator(opts.appid); +}; diff --git a/samples/ToDoLite/app/config.json b/samples/ToDoLite/app/config.json new file mode 100644 index 0000000..031d1f8 --- /dev/null +++ b/samples/ToDoLite/app/config.json @@ -0,0 +1,13 @@ +{ + "global": { + "dbname": "todos4" + }, + "env:development": {}, + "env:test": {}, + "env:production": {}, + "os:android": {}, + "os:blackberry": {}, + "os:ios": {}, + "os:mobileweb": {}, + "dependencies": {} +} \ No newline at end of file diff --git a/samples/ToDoLite/app/controllers/detail.js b/samples/ToDoLite/app/controllers/detail.js new file mode 100644 index 0000000..5259443 --- /dev/null +++ b/samples/ToDoLite/app/controllers/detail.js @@ -0,0 +1,166 @@ +var args = arguments[0] || {}; + +var imageForNewTask = null; +var list = null; + +// data binding + +function transform(model) { + var result = model.toJSON(); + var att = model.attachmentNamed('image.jpg'); + result.image = (att && att.content) || "/images/Camera-Light.png"; + result.template = result.checked ? 'complete' : 'incomplete'; + return result; +} + +// helper functions + +function displayAddImageActionSheet(listItem) { + var task = listItem ? $.tasks.at(listItem.itemIndex) : null; + + var options = []; + if (Ti.Media.isCameraSupported) { + options.push("Take Picture"); + } + options.push("Choose Existing"); + if (imageForNewTask || (task && task.attachmentNamed('image.jpg'))) { + options.push("Delete"); + } + options.push("Cancel"); + var dialog = Ti.UI.createOptionDialog({ + options: options, + cancel: options.length - 1, + }); + dialog.addEventListener('click', function(e) { + var selected = options[e.index]; + if (selected === 'Take Picture') { + takePicture(listItem); + } + else if (selected == 'Choose Existing') { + chooseExistingPhoto(listItem); + } + else if (selected == 'Delete') { + if (task) { + task.removeAttachment('image.jpg'); + } + else { + imageForNewTask = null; + updateAddImageButtonWithImage(null); + } + } + }); + dialog.show(); +} + +function updateAddImageButtonWithImage(img) { + $.addImageButton.image = img || '/images/Camera.png'; +} + +function takePicture(listItem) { + Ti.Media.showCamera({ + success: function(e) { + if (listItem) { + var task = $.tasks.at(listItem.itemIndex); + task.addAttachment('image.jpg', e.media.mimeType, e.media); + listItem.image = e.media; + } + else { + imageForNewTask = e.media; + updateAddImageButtonWithImage(imageForNewTask); + } + } + }); +} + +function chooseExistingPhoto(listItem) { + Ti.Media.openPhotoGallery({ + success: function(e) { + if (listItem) { + var task = $.tasks.at(listItem.itemIndex); + task.addAttachment('image.jpg', e.media.mimeType, e.media); + listItem.image = e.media; + } + else { + imageForNewTask = e.media; + updateAddImageButtonWithImage(imageForNewTask); + } + } + }); +} + +function updateModels() { + $.tasks.fetch({ startKey: [args.list_id], endKey: [args.list_id, {}] }); + list = Alloy.createModel('list'); + list.fetch({ id: args.list_id }); +} + +// event handlers + +function windowOpen(e) { + updateAddImageButtonWithImage(); + updateModels(); +} + +function windowClose(e) { + $.destroy(); +} + +function shareButtonAction(e) { + Alloy.Globals.loginAndSync(function() { + Ti.App.fireEvent('list:share', { list_id: args.list_id }); + }); +} + +// set image for new task +function addImageButtonAction(e) { + e.cancelBubble = true; + displayAddImageActionSheet(); +} + +// set image for existing task +function imageButtonAction(e) { + e.cancelBubble = true; // doesn't work: https://jira.appcelerator.org/browse/TIMOB-16898 + var task = $.tasks.at(e.itemIndex); + if (task.attachmentNamed('image.jpg')) { + var controller = Alloy.createController('image', { task_id: task.id }); + controller.getView().open({ modal:true }); + } + else { + displayAddImageActionSheet(task); + } +} + +function textFieldShouldReturn(e) { + var title = e.value; + if (title.length == 0) { + return; + } + + // create and save a new task + var task = list.addTask(title, imageForNewTask); + if (task) { + imageForNewTask = null; + updateAddImageButtonWithImage(null); + $.addItemTextField.value = ''; + $.tasks.add(task); + } +} + +function didSelectRow(e) { + var task = $.tasks.at(e.itemIndex); + task.set({ checked: !task.get('checked') }); + task.save(); + + var checked = task.get('checked'); + + var listItem = e.section.items[e.itemIndex]; + listItem.template = checked ? 'complete' : 'incomplete'; + e.section.updateItemAt(e.itemIndex, listItem, { animated: true }); +} + +function didDelete(e) { + alert('delete '+e.itemId); + var doomed = $.tasks.at(e.itemIndex); + doomed.deleteTask(); + updateModels(); +} diff --git a/samples/ToDoLite/app/controllers/image.js b/samples/ToDoLite/app/controllers/image.js new file mode 100644 index 0000000..0393915 --- /dev/null +++ b/samples/ToDoLite/app/controllers/image.js @@ -0,0 +1,21 @@ +var args = arguments[0] || {}; + +function windowOpen(e) { + var task = Alloy.createModel('task'); + task.fetch({ + id: args.task_id, + success: function() { + var att = task.attachmentNamed('image.jpg'); + if (att) { + $.imageView.image = att.content; + } + else { + // TODO close, error? + } + } + }); +} + +function dismissWindow(e) { + $.win.close(); +} diff --git a/samples/ToDoLite/app/controllers/index.js b/samples/ToDoLite/app/controllers/index.js new file mode 100644 index 0000000..a8d5ff1 --- /dev/null +++ b/samples/ToDoLite/app/controllers/index.js @@ -0,0 +1,12 @@ + +Ti.App.addEventListener('list:select', function(e) { + var controller = Alloy.createController('detail', e); + $.index.openWindow(controller.getView()); +}); + +Ti.App.addEventListener('list:share', function(e) { + var controller = Alloy.createController('share', e); + $.index.openWindow(controller.getView()); +}); + +$.index.open(); diff --git a/samples/ToDoLite/app/controllers/master.js b/samples/ToDoLite/app/controllers/master.js new file mode 100644 index 0000000..f704aab --- /dev/null +++ b/samples/ToDoLite/app/controllers/master.js @@ -0,0 +1,73 @@ +var lists = Alloy.Collections.list; + +function createListWithTitle(title) { + var list = Alloy.createModel('List'); + // TODO if there is a userID set, add it to the list + list.save({ title: title }, { + success: function() { + lists.fetch(); + }, + error: function(e) { + Ti.UI.createAlertDialog({ + title: "Error", + message: "Cannot create a new list.", + }).show(); + } + }); +} + +// event handlers + +function insertNewObject(e) { + // TODO use androidView property for Android + var dialog = Ti.UI.createAlertDialog({ + title: "New To-Do List", + message: "Title for new list:", + style: Ti.UI.iPhone.AlertDialogStyle.PLAIN_TEXT_INPUT, + buttonNames: ['Cancel', 'Create'], + cancel: 0, + }); + dialog.addEventListener('click', function(e) { + if (e.index !== e.source.cancel) { + if (e.text && e.text.length > 0) { + createListWithTitle(e.text); + } + } + }); + dialog.show(); +} + +function didSelectRow(e) { + Ti.App.fireEvent('list:select', { list_id: e.itemId }); +} + +function didDelete(e) { + var doomed = lists.get(e.itemId); + doomed.deleteList(); + lists.fetch(); +} + +function windowOpen(e) { + if (!Alloy.Globals.cblSync.userID) { + var loginButton = Ti.UI.createButton({ + title: "Login", + }); + loginButton.addEventListener('click', function() { + Alloy.Globals.loginAndSync(function() { + $.master.leftNavButton = null; + }); + }); + $.master.leftNavButton = loginButton; + } + Ti.App.addEventListener('sync:change', syncChanged); + lists.fetch(); +} + +function windowClose(e) { + Ti.App.removeEventListener('sync:change', syncChanged); + $.destroy(); +} + +function syncChanged(e) { + lists.fetch(); +} diff --git a/samples/ToDoLite/app/controllers/share.js b/samples/ToDoLite/app/controllers/share.js new file mode 100644 index 0000000..9ff7979 --- /dev/null +++ b/samples/ToDoLite/app/controllers/share.js @@ -0,0 +1,59 @@ +var args = arguments[0] || {}; + +var list; +var myDocId; + +// corresponds to couchTableSource:willUseCell:forRow: +function transform(model) { + var result = model.toJSON(); + var personId = model.id; + var member = false; + + if (myDocId === personId) { + member = true; + } + else { + member = _.contains(list.members || [], personId); + } + + result.accessoryType = member ? Ti.UI.LIST_ACCESSORY_TYPE_CHECKMARK : Ti.UI.LIST_ACCESSORY_TYPE_NONE; + + return result; +} + +function windowOpen() { + if (!Alloy.Globals.cblSync.userID) { + throw('no userID'); + } + + myDocId = 'p:' + Alloy.Globals.cblSync.userID; + configureView(); +} + +function windowClose() { + $.destroy(); +} + +function didSelectRow(e) { + var toggleMemberId = e.itemId; + var members = list.get('members') || []; + var x = members.indexOf(toggleMemberId); + if (x < 0) { + // add to array + members.push(toggleMemberId); + } + else { + // remove from array + members.splice(x, 1); + } + list.save({ members: members }); + + // don't need to call configureView() again +} + +function configureView() { + list = Alloy.createModel('list'); + list.fetch({ id: args.list_id }); + + $.profiles.fetch(); +} diff --git a/samples/ToDoLite/app/models/List.js b/samples/ToDoLite/app/models/List.js new file mode 100644 index 0000000..1fc7ff8 --- /dev/null +++ b/samples/ToDoLite/app/models/List.js @@ -0,0 +1,87 @@ +exports.definition = { + + config: { + adapter: { + type: 'titouchdb', + dbname: Alloy.CFG.dbname, + views: [ + { + name: 'lists', + version: '1', + map: function(doc) { + if (doc.type == 'list') { + emit(doc.title, null); + } + } + }, + { + name: 'tasks_by_list', + version: '1', + map: function(doc) { + if (doc.type == 'task') { + emit(doc.list_id, null); + } + } + } + ], + view_options: { + prefetch: true + }, + static_properties: { + type: 'list' + } + } + }, + + extendModel: function(Model) { + _.extend(Model.prototype, { + addTask: function(title, image) { + var list_id = this.id; + var task = Alloy.createModel('task', { + title: title, + created_at: new Date().getTime(), + list_id: list_id + }); + task.save(); + if (image) { + task.addAttachment('image.jpg', image.mimeType, image); + } + return task; + }, + deleteList: function() { + // delete the list document and all task documents associated with it + var view = this.database.getExistingView('tasks_by_list'); + var query = view.createQuery(); + query.startKey = this.id; + query.endKey = this.id + '\uFFFF'; + var rows = query.run(); + while (row = rows.next()) { + row.getDocument().deleteDocument(); + } + this.database.getExistingDocument(this.id).deleteDocument(); + } + }); + return Model; + }, + + extendCollection: function(Collection) { + _.extend(Collection.prototype, { + map_row: function(Model, row) { + var result = new Model(row.documentProperties); + // add custom properties here, if any + return result; + }, + updateAllListsWithOwner: function(owner) { + var view = this.database.getExistingView('lists'); + var query = view.createQuery(); + var rows = query.run(); + while (row = rows.next()) { + var doc = row.getDocument(); + doc.putProperties(_.extend(doc.properties, { owner: owner })); + } + } + }); + return Collection; + } +}; + diff --git a/samples/ToDoLite/app/models/Profile.js b/samples/ToDoLite/app/models/Profile.js new file mode 100644 index 0000000..a6fc4b2 --- /dev/null +++ b/samples/ToDoLite/app/models/Profile.js @@ -0,0 +1,45 @@ +exports.definition = { + + config: { + adapter: { + type: "titouchdb", + dbname: Alloy.CFG.dbname, + views: [ + { + name: 'profiles', + version: '1', + map: function(doc) { + if (doc.type == 'profile') { + emit(doc.name, null); + } + } + } + ], + view_options: { + prefetch: true + }, + static_properties: { + type: 'profile' + } + } + }, + + extendModel: function(Model) { + _.extend(Model.prototype, { + // TODO maybe set all tasks and lists to this profile? + }); + return Model; + }, + + extendCollection: function(Collection) { + _.extend(Collection.prototype, { + map_row: function(Model, row) { + var result = new Model(row.documentProperties); + // add custom properties here, if any + return result; + } + }); + return Collection; + } +}; + diff --git a/samples/ToDoLite/app/models/Task.js b/samples/ToDoLite/app/models/Task.js new file mode 100644 index 0000000..429242f --- /dev/null +++ b/samples/ToDoLite/app/models/Task.js @@ -0,0 +1,47 @@ +exports.definition = { + + config: { + adapter: { + type: "titouchdb", + dbname: Alloy.CFG.dbname, + views: [ + { + name: "tasksByDate", + version: '1', + map: function(doc) { + if (doc.type == 'task') { + emit([doc.list_id, doc.created_at], null); + } + } + } + ], + view_options: { + prefetch: true + }, + static_properties: { + type: 'task' + } + } + }, + + extendModel: function(Model) { + _.extend(Model.prototype, { + deleteTask: function() { + this.database.getExistingDocument(this.id).deleteDocument(); + } + }); + return Model; + }, + + extendCollection: function(Collection) { + _.extend(Collection.prototype, { + map_row: function(Model, row) { + var result = new Model(row.documentProperties); + // add custom properties here, if any + return result; + } + }); + return Collection; + } +}; + diff --git a/samples/ToDoLite/app/styles/app.tss b/samples/ToDoLite/app/styles/app.tss new file mode 100644 index 0000000..dbdf1dc --- /dev/null +++ b/samples/ToDoLite/app/styles/app.tss @@ -0,0 +1,4 @@ +"Window": { + backgroundColor: "white" +} + diff --git a/samples/ToDoLite/app/styles/detail.tss b/samples/ToDoLite/app/styles/detail.tss new file mode 100644 index 0000000..20d84ba --- /dev/null +++ b/samples/ToDoLite/app/styles/detail.tss @@ -0,0 +1,9 @@ + + +".complete": { + color: 'grey', +} + +".incomplete": { + color: 'black', +} diff --git a/samples/ToDoLite/app/views/detail.xml b/samples/ToDoLite/app/views/detail.xml new file mode 100644 index 0000000..d2f81a3 --- /dev/null +++ b/samples/ToDoLite/app/views/detail.xml @@ -0,0 +1,33 @@ + + + + +