Skip to content
Browse files

Improvements to pass CouchDB bulk_docs.js unit test

* Support _bulk_docs.
* Support ?revs=true mode when getting documents.
* -[TDDatabase putRevision:...] now has a mode where it is allowed to create conflicts (needed for all_or_nothing mode of _bulk_docs.)
* Better defense against inserting invalid doc IDs (empty or _-prefixed).
* Recognize design-document URL when encoded as _design%2F...
  • Loading branch information...
1 parent 2ae2a10 commit 666dbcaecf6cfd99c084bd55f676af8aa16b95ce @snej snej committed Jan 5, 2012
Showing with 315 additions and 168 deletions.
  1. +100 −54 Source/TDDatabase+Insertion.m
  2. +12 −2 Source/TDDatabase.h
  3. +98 −1 Source/TDDatabase.m
  4. +1 −93 Source/TDPusher.m
  5. +87 −14 Source/TDRouter+Handlers.m
  6. +1 −0 Source/TDRouter.h
  7. +16 −4 Source/TDRouter.m
View
154 Source/TDDatabase+Insertion.m
@@ -49,9 +49,12 @@ - (TDStatus) validateRevision: (TDRevision*)newRev previousRevision: (TDRevision
@implementation TDDatabase (Insertion)
+#pragma mark - DOCUMENT & REV IDS:
+
+
+ (BOOL) isValidDocumentID: (NSString*)str {
// http://wiki.apache.org/couchdb/HTTP_Document_API#Documents
- return (str.length > 0);
+ return str.length > 0 && ([str characterAtIndex: 0] != '_' || [str hasPrefix: @"_design/"]);
}
@@ -62,10 +65,13 @@ + (BOOL) isValidDocumentID: (NSString*)str {
return [str autorelease];
}
+/** Generates a new document ID at random. */
+ (NSString*) generateDocumentID {
return createUUID();
}
+
+/** Given an existing revision ID, generates an ID for the next revision. */
- (NSString*) generateNextRevisionID: (NSString*)revID {
// Revision IDs have a generation count, a hyphen, and a UUID.
unsigned generation = 0;
@@ -79,23 +85,16 @@ - (NSString*) generateNextRevisionID: (NSString*)revID {
}
-- (void) notifyChange: (TDRevision*)rev source: (NSURL*)source
-{
- NSDictionary* userInfo = $dict({@"rev", rev},
- {@"seq", $object(rev.sequence)},
- {@"source", source});
- [[NSNotificationCenter defaultCenter] postNotificationName: TDDatabaseChangeNotification
- object: self
- userInfo: userInfo];
-}
-
-
+/** Adds a new document ID to the 'docs' table. */
- (SInt64) insertDocumentID: (NSString*)docID {
+ Assert([TDDatabase isValidDocumentID: docID]); // this should be caught before I get here
if (![_fmdb executeUpdate: @"INSERT INTO docs (docid) VALUES (?)", docID])
return -1;
return _fmdb.lastInsertRowId;
}
+
+/** Maps a document ID to a numeric ID (row # in 'docs'), creating a new row if needed. */
- (SInt64) getOrInsertDocNumericID: (NSString*)docID {
SInt64 docNumericID = [self getDocNumericID: docID];
if (docNumericID == 0)
@@ -104,6 +103,26 @@ - (SInt64) getOrInsertDocNumericID: (NSString*)docID {
}
+/** Extracts the history of revision IDs (in reverse chronological order) from the _revisions key */
++ (NSArray*) parseCouchDBRevisionHistory: (NSDictionary*)docProperties {
+ NSDictionary* revisions = $castIf(NSDictionary,
+ [docProperties objectForKey: @"_revisions"]);
+ if (!revisions)
+ return nil;
+ // Extract the history, expanding the numeric prefixes:
+ __block int start = [$castIf(NSNumber, [revisions objectForKey: @"start"]) intValue];
+ NSArray* revIDs = $castIf(NSArray, [revisions objectForKey: @"ids"]);
+ return [revIDs my_map: ^(id revID) {
+ return (start ? $sprintf(@"%d-%@", start--, revID) : revID);
+ }];
+}
+
+
+#pragma mark - INSERTION:
+
+
+/** Returns the JSON to be stored into the 'json' column for a given TDRevision.
+ This has all the special keys like "_id" stripped out. */
- (NSData*) encodeDocumentJSON: (TDRevision*)rev {
static NSSet* sKnownSpecialKeys;
if (!sKnownSpecialKeys) {
@@ -137,6 +156,18 @@ - (NSData*) encodeDocumentJSON: (TDRevision*)rev {
}
+/** Posts a local NSNotification of a new revision of a document. */
+- (void) notifyChange: (TDRevision*)rev source: (NSURL*)source
+{
+ NSDictionary* userInfo = $dict({@"rev", rev},
+ {@"seq", $object(rev.sequence)},
+ {@"source", source});
+ [[NSNotificationCenter defaultCenter] postNotificationName: TDDatabaseChangeNotification
+ object: self
+ userInfo: userInfo];
+}
+
+
// Raw row insertion. Returns new sequence, or 0 on error
- (SequenceNumber) insertRevision: (TDRevision*)rev
docNumericID: (SInt64)docNumericID
@@ -157,15 +188,26 @@ - (SequenceNumber) insertRevision: (TDRevision*)rev
}
+/** Public method to add a new revision of a document. */
+- (TDRevision*) putRevision: (TDRevision*)rev
+ prevRevisionID: (NSString*)prevRevID // rev ID being replaced, or nil if an insert
+ status: (TDStatus*)outStatus
+{
+ return [self putRevision: rev prevRevisionID: prevRevID allowConflict: NO status: outStatus];
+}
+
+
+/** Public method to add a new revision of a document. */
- (TDRevision*) putRevision: (TDRevision*)rev
prevRevisionID: (NSString*)prevRevID // rev ID being replaced, or nil if an insert
+ allowConflict: (BOOL)allowConflict
status: (TDStatus*)outStatus
{
Assert(outStatus);
NSString* docID = rev.docID;
- SInt64 docNumericID;
BOOL deleted = rev.deleted;
- if (!rev || (prevRevID && !docID) || (deleted && !docID)) {
+ if (!rev || (prevRevID && !docID) || (deleted && !docID)
+ || (docID && ![TDDatabase isValidDocumentID: docID])) {
*outStatus = 400;
return nil;
}
@@ -174,18 +216,24 @@ - (TDRevision*) putRevision: (TDRevision*)rev
[self beginTransaction];
FMResultSet* r = nil;
@try {
+ SInt64 docNumericID = docID ? [self getDocNumericID: docID] : 0;
SequenceNumber parentSequence = 0;
if (prevRevID) {
// Replacing: make sure given prevRevID is current & find its sequence number:
- docNumericID = [self getOrInsertDocNumericID: docID];
- if (docNumericID <= 0)
+ if (docNumericID <= 0) {
+ *outStatus = 404;
return nil;
- parentSequence = [_fmdb longLongForQuery: @"SELECT sequence FROM revs "
- "WHERE doc_id=? AND revid=? and current=1 LIMIT 1",
- $object(docNumericID), prevRevID];
+ }
+ NSString* sql = $sprintf(@"SELECT sequence FROM revs "
+ "WHERE doc_id=? AND revid=? %@ LIMIT 1",
+ (allowConflict ? @"" : @"AND current=1"));
+ parentSequence = [_fmdb longLongForQuery: sql, $object(docNumericID), prevRevID];
if (parentSequence == 0) {
// Not found: 404 or a 409, depending on whether there is any current revision
- *outStatus = [self existsDocumentWithID: docID revisionID: nil] ? 409 : 404;
+ if (!allowConflict && [self existsDocumentWithID: docID revisionID: nil])
+ *outStatus = 409;
+ else
+ *outStatus = 404;
return nil;
}
@@ -211,35 +259,41 @@ - (TDRevision*) putRevision: (TDRevision*)rev
*outStatus = [self existsDocumentWithID: docID revisionID: nil] ? 409 : 404;
return nil;
}
- // Inserting first revision, with docID given: make sure docID doesn't exist,
- // or exists but is currently deleted
+ // Inserting first revision, with docID given. First validate:
if (![self validateRevision: rev previousRevision: nil]) {
*outStatus = 403;
return nil;
}
- docNumericID = [self getOrInsertDocNumericID: docID];
- if (docNumericID <= 0)
- return nil;
- r = [_fmdb executeQuery: @"SELECT sequence, deleted FROM revs "
- "WHERE doc_id=? and current=1 ORDER BY revid DESC LIMIT 1",
- $object(docNumericID)];
- if (!r)
- return nil;
- if ([r next]) {
- if ([r boolForColumnIndex: 1]) {
- // Make the deleted revision no longer current:
- if (![_fmdb executeUpdate: @"UPDATE revs SET current=0 WHERE sequence=?",
- $object([r longLongIntForColumnIndex: 0])])
- return nil;
- } else {
- *outStatus = 409;
+
+ if (docNumericID <= 0) {
+ // Doc doesn't exist at all; create it:
+ docNumericID = [self insertDocumentID: docID];
+ if (docNumericID <= 0)
+ return nil;
+ } else {
+ // Doc exists; check whether current winning revision is deleted:
+ r = [_fmdb executeQuery: @"SELECT sequence, deleted FROM revs "
+ "WHERE doc_id=? and current=1 ORDER BY revid DESC LIMIT 1",
+ $object(docNumericID)];
+ if (!r)
return nil;
+ if ([r next]) {
+ if ([r boolForColumnIndex: 1]) {
+ // Make the deleted revision no longer current:
+ if (![_fmdb executeUpdate: @"UPDATE revs SET current=0 WHERE sequence=?",
+ $object([r longLongIntForColumnIndex: 0])])
+ return nil;
+ } else if (!allowConflict) {
+ // The current winning revision is not deleted, so this is a conflict
+ *outStatus = 409;
+ return nil;
+ }
}
+ [r close];
+ r = nil;
}
- [r close];
- r = nil;
} else {
- // Inserting first revision, with no docID given: generate a unique docID:
+ // Inserting first revision, with no docID given (POST): generate a unique docID:
docID = [[self class] generateDocumentID];
docNumericID = [self insertDocumentID: docID];
if (docNumericID <= 0)
@@ -293,19 +347,22 @@ - (TDRevision*) putRevision: (TDRevision*)rev
}
+/** Public method to add an existing revision of a document (probably being pulled). */
- (TDStatus) forceInsert: (TDRevision*)rev
revisionHistory: (NSArray*)history // in *reverse* order, starting with rev's revID
source: (NSURL*)source
{
NSUInteger historyCount = history.count;
if (historyCount < 1 || !$equal([history objectAtIndex: 0], rev.revID))
return 400;
+ NSString* docID = rev.docID;
+ if (![TDDatabase isValidDocumentID: docID])
+ return 400;
BOOL success = NO;
[self beginTransaction];
@try {
// First look up all locally-known revisions of this document:
- NSString* docID = rev.docID;
SInt64 docNumericID = [self getOrInsertDocNumericID: docID];
TDRevisionList* localRevs = [self getAllRevisionsOfDocumentID: docID
numericID: docNumericID
@@ -399,18 +456,7 @@ - (TDStatus) forceInsert: (TDRevision*)rev
}
-+ (NSArray*) parseCouchDBRevisionHistory: (NSDictionary*)docProperties {
- NSDictionary* revisions = $castIf(NSDictionary,
- [docProperties objectForKey: @"_revisions"]);
- if (!revisions)
- return nil;
- // Extract the history, expanding the numeric prefixes:
- __block int start = [$castIf(NSNumber, [revisions objectForKey: @"start"]) intValue];
- NSArray* revIDs = $castIf(NSArray, [revisions objectForKey: @"ids"]);
- return [revIDs my_map: ^(id revID) {
- return (start ? $sprintf(@"%d-%@", start--, revID) : revID);
- }];
-}
+#pragma mark - VALIDATION:
- (void) defineValidation: (NSString*)validationName asBlock: (TDValidationBlock)validationBlock {
View
14 Source/TDDatabase.h
@@ -35,8 +35,9 @@ typedef unsigned TDContentOptions;
enum {
kTDIncludeAttachments = 1,
kTDIncludeConflicts = 2,
- kTDIncludeRevsInfo = 4,
- kTDIncludeLocalSeq = 8
+ kTDIncludeRevs = 4,
+ kTDIncludeRevsInfo = 8,
+ kTDIncludeLocalSeq = 16
};
@@ -107,6 +108,9 @@ extern const TDChangesOptions kDefaultTDChangesOptions;
starting with the given revision. */
- (NSArray*) getRevisionHistory: (TDRevision*)rev;
+/** Returns the revision history as a _revisions dictionary, as returned by the REST API's ?revs=true option. */
+- (NSDictionary*) getRevisionHistoryDict: (TDRevision*)rev;
+
/** Returns all the known revisions (or all current/conflicting revisions) of a document. */
- (TDRevisionList*) getAllRevisionsOfDocumentID: (NSString*)docID
onlyCurrent: (BOOL)onlyCurrent;
@@ -140,10 +144,16 @@ extern const TDChangesOptions kDefaultTDChangesOptions;
/** Stores a new (or initial) revision of a document. This is what's invoked by a PUT or POST. As with those, the previous revision ID must be supplied when necessary and the call will fail if it doesn't match.
@param revision The revision to add. If the docID is nil, a new UUID will be assigned. Its revID must be nil. It must have a JSON body.
@param prevRevID The ID of the revision to replace (same as the "?rev=" parameter to a PUT), or nil if this is a new document.
+ @param allowConflict If NO, an error status 409 will be returned if the insertion would create a conflict, i.e. if the previous revision already has a child.
@param status On return, an HTTP status code indicating success or failure.
@return A new TDRevision with the docID, revID and sequence filled in (but no body). */
- (TDRevision*) putRevision: (TDRevision*)revision
prevRevisionID: (NSString*)prevRevID
+ allowConflict: (BOOL)allowConflict
+ status: (TDStatus*)outStatus;
+
+- (TDRevision*) putRevision: (TDRevision*)revision
+ prevRevisionID: (NSString*)prevRevID
status: (TDStatus*)outStatus;
/** Inserts an already-existing revision replicated from a remote database. It must already have a revision ID. This may create a conflict! The revision's history must be given; ancestor revision IDs that don't already exist locally will create phantom revisions with no content. */
View
99 Source/TDDatabase.m
@@ -316,10 +316,14 @@ - (NSDictionary*) extraPropertiesForRevision: (TDRevision*)rev options: (TDConte
// Get more optional stuff to put in the properties:
//OPT: This probably ends up making redundant SQL queries if multiple options are enabled.
- id localSeq=nil, revsInfo=nil, conflicts=nil;
+ id localSeq=nil, revs=nil, revsInfo=nil, conflicts=nil;
if (options & kTDIncludeLocalSeq)
localSeq = $object(sequence);
+ if (options & kTDIncludeRevs) {
+ revs = [self getRevisionHistoryDict: rev];
+ }
+
if (options & kTDIncludeRevsInfo) {
revsInfo = [[self getRevisionHistory: rev] my_map: ^id(id rev) {
NSString* status = @"available";
@@ -344,6 +348,7 @@ - (NSDictionary*) extraPropertiesForRevision: (TDRevision*)rev options: (TDConte
{@"_deleted", (rev.deleted ? $true : nil)},
{@"_attachments", attachmentsDict},
{@"_local_seq", localSeq},
+ {@"_revisions", revs},
{@"_revs_info", revsInfo},
{@"_conflicts", conflicts});
}
@@ -549,6 +554,52 @@ - (NSArray*) getRevisionHistory: (TDRevision*)rev {
}
+// Splits a revision ID into its generation number and opaque suffix string
+static BOOL parseRevID( NSString* revID, int* outNum, NSString** outSuffix) {
+ NSScanner* scanner = [[NSScanner alloc] initWithString: revID];
+ scanner.charactersToBeSkipped = nil;
+ BOOL parsed = [scanner scanInt: outNum] && [scanner scanString: @"-" intoString: nil];
+ *outSuffix = [revID substringFromIndex: scanner.scanLocation];
+ [scanner release];
+ return parsed && *outNum > 0 && (*outSuffix).length > 0;
+}
+
+static NSDictionary* makeRevisionHistoryDict(NSArray* history) {
+ if (!history)
+ return nil;
+
+ // Try to extract descending numeric prefixes:
+ NSMutableArray* suffixes = $marray();
+ id start = nil;
+ int lastRevNo = -1;
+ for (TDRevision* rev in history) {
+ int revNo;
+ NSString* suffix;
+ if (parseRevID(rev.revID, &revNo, &suffix)) {
+ if (!start)
+ start = $object(revNo);
+ else if (revNo != lastRevNo - 1) {
+ start = nil;
+ break;
+ }
+ lastRevNo = revNo;
+ [suffixes addObject: suffix];
+ } else {
+ start = nil;
+ break;
+ }
+ }
+
+ NSArray* revIDs = start ? suffixes : [history my_map: ^(id rev) {return [rev revID];}];
+ return $dict({@"ids", revIDs}, {@"start", start});
+}
+
+- (NSDictionary*) getRevisionHistoryDict: (TDRevision*)rev {
+ return makeRevisionHistoryDict([self getRevisionHistory: rev]);
+}
+
+
+
const TDChangesOptions kDefaultTDChangesOptions = {UINT_MAX, 0, NO, NO, YES};
@@ -761,3 +812,49 @@ - (NSDictionary*) getAllDocs: (const TDQueryOptions*)options {
@end
+
+
+
+
+#if DEBUG
+
+static TDRevision* mkrev(NSString* revID) {
+ return [[[TDRevision alloc] initWithDocID: @"docid" revID: revID deleted: NO] autorelease];
+}
+
+TestCase(TDDatabase_ParseRevID) {
+ RequireTestCase(TDDatabase);
+ int num;
+ NSString* suffix;
+ CAssert(parseRevID(@"1-utiopturoewpt", &num, &suffix));
+ CAssertEq(num, 1);
+ CAssertEqual(suffix, @"utiopturoewpt");
+
+ CAssert(parseRevID(@"321-fdjfdsj-e", &num, &suffix));
+ CAssertEq(num, 321);
+ CAssertEqual(suffix, @"fdjfdsj-e");
+
+ CAssert(!parseRevID(@"0-fdjfdsj-e", &num, &suffix));
+ CAssert(!parseRevID(@"-4-fdjfdsj-e", &num, &suffix));
+ CAssert(!parseRevID(@"5_fdjfdsj-e", &num, &suffix));
+ CAssert(!parseRevID(@" 5-fdjfdsj-e", &num, &suffix));
+ CAssert(!parseRevID(@"7 -foo", &num, &suffix));
+ CAssert(!parseRevID(@"7-", &num, &suffix));
+ CAssert(!parseRevID(@"7", &num, &suffix));
+ CAssert(!parseRevID(@"eiuwtiu", &num, &suffix));
+ CAssert(!parseRevID(@"", &num, &suffix));
+}
+
+TestCase(TDDatabase_MakeRevisionHistoryDict) {
+ NSArray* revs = $array(mkrev(@"4-jkl"), mkrev(@"3-ghi"), mkrev(@"2-def"));
+ CAssertEqual(makeRevisionHistoryDict(revs), $dict({@"ids", $array(@"jkl", @"ghi", @"def")},
+ {@"start", $object(4)}));
+
+ revs = $array(mkrev(@"4-jkl"), mkrev(@"2-def"));
+ CAssertEqual(makeRevisionHistoryDict(revs), $dict({@"ids", $array(@"4-jkl", @"2-def")}));
+
+ revs = $array(mkrev(@"12345"), mkrev(@"6789"));
+ CAssertEqual(makeRevisionHistoryDict(revs), $dict({@"ids", $array(@"12345", @"6789")}));
+}
+
+#endif
View
94 Source/TDPusher.m
@@ -19,9 +19,6 @@
#import "TDInternal.h"
-static NSDictionary* makeCouchRevisionList( NSArray* history );
-
-
@implementation TDPusher
@@ -112,8 +109,7 @@ - (void) processInbox: (TDRevisionList*)changes {
}
// Add the _revisions list:
- [properties setValue: makeCouchRevisionList([_db getRevisionHistory: rev])
- forKey: @"_revisions"];
+ [properties setValue: [_db getRevisionHistoryDict: rev] forKey: @"_revisions"];
}
return [properties autorelease];
}];
@@ -139,92 +135,4 @@ - (void) processInbox: (TDRevisionList*)changes {
}
-// Splits a revision ID into its generation number and opaque suffix string
-static BOOL parseRevID( NSString* revID, int* outNum, NSString** outSuffix) {
- NSScanner* scanner = [[NSScanner alloc] initWithString: revID];
- scanner.charactersToBeSkipped = nil;
- BOOL parsed = [scanner scanInt: outNum] && [scanner scanString: @"-" intoString: nil];
- *outSuffix = [revID substringFromIndex: scanner.scanLocation];
- [scanner release];
- return parsed && *outNum > 0 && (*outSuffix).length > 0;
-}
-
-
-static NSDictionary* makeCouchRevisionList( NSArray* history ) {
- if (!history)
- return nil;
-
- // Try to extract descending numeric prefixes:
- NSMutableArray* suffixes = $marray();
- id start = nil;
- int lastRevNo = -1;
- for (TDRevision* rev in history) {
- int revNo;
- NSString* suffix;
- if (parseRevID(rev.revID, &revNo, &suffix)) {
- if (!start)
- start = $object(revNo);
- else if (revNo != lastRevNo - 1) {
- start = nil;
- break;
- }
- lastRevNo = revNo;
- [suffixes addObject: suffix];
- } else {
- start = nil;
- break;
- }
- }
-
- NSArray* revIDs = start ? suffixes : [history my_map: ^(id rev) {return [rev revID];}];
- return $dict({@"ids", revIDs}, {@"start", start});
-}
-
-
@end
-
-
-
-
-#if DEBUG
-
-static TDRevision* mkrev(NSString* revID) {
- return [[[TDRevision alloc] initWithDocID: @"docid" revID: revID deleted: NO] autorelease];
-}
-
-TestCase(TDPusher_ParseRevID) {
- RequireTestCase(TDDatabase);
- int num;
- NSString* suffix;
- CAssert(parseRevID(@"1-utiopturoewpt", &num, &suffix));
- CAssertEq(num, 1);
- CAssertEqual(suffix, @"utiopturoewpt");
-
- CAssert(parseRevID(@"321-fdjfdsj-e", &num, &suffix));
- CAssertEq(num, 321);
- CAssertEqual(suffix, @"fdjfdsj-e");
-
- CAssert(!parseRevID(@"0-fdjfdsj-e", &num, &suffix));
- CAssert(!parseRevID(@"-4-fdjfdsj-e", &num, &suffix));
- CAssert(!parseRevID(@"5_fdjfdsj-e", &num, &suffix));
- CAssert(!parseRevID(@" 5-fdjfdsj-e", &num, &suffix));
- CAssert(!parseRevID(@"7 -foo", &num, &suffix));
- CAssert(!parseRevID(@"7-", &num, &suffix));
- CAssert(!parseRevID(@"7", &num, &suffix));
- CAssert(!parseRevID(@"eiuwtiu", &num, &suffix));
- CAssert(!parseRevID(@"", &num, &suffix));
-}
-
-TestCase(TDPusher_RevisionList) {
- NSArray* revs = $array(mkrev(@"4-jkl"), mkrev(@"3-ghi"), mkrev(@"2-def"));
- CAssertEqual(makeCouchRevisionList(revs), $dict({@"ids", $array(@"jkl", @"ghi", @"def")},
- {@"start", $object(4)}));
-
- revs = $array(mkrev(@"4-jkl"), mkrev(@"2-def"));
- CAssertEqual(makeCouchRevisionList(revs), $dict({@"ids", $array(@"4-jkl", @"2-def")}));
-
- revs = $array(mkrev(@"12345"), mkrev(@"6789"));
- CAssertEqual(makeCouchRevisionList(revs), $dict({@"ids", $array(@"12345", @"6789")}));
-}
-
-#endif
View
101 Source/TDRouter+Handlers.m
@@ -16,7 +16,15 @@
@interface TDRouter ()
-- (TDStatus) update: (TDDatabase*)db docID: (NSString*)docID json: (NSData*)json
+- (TDStatus) update: (TDDatabase*)db
+ docID: (NSString*)docID
+ body: (TDBody*)body
+ deleting: (BOOL)deleting
+ allowConflict: (BOOL)allowConflict
+ createdRev: (TDRevision**)outRev;
+- (TDStatus) update: (TDDatabase*)db
+ docID: (NSString*)docID
+ json: (NSData*)json
deleting: (BOOL)deleting;
@end
@@ -44,8 +52,8 @@ - (TDStatus) do_GET_all_dbs {
- (TDStatus) do_POST_replicate {
// Extract the parameters from the JSON request body:
// http://wiki.apache.org/couchdb/Replication
- id body = [NSJSONSerialization JSONObjectWithData: _request.HTTPBody options: 0 error: nil];
- if (![body isKindOfClass: [NSDictionary class]])
+ id body = self.bodyAsDictionary;
+ if (!body)
return 400;
NSString* source = $castIf(NSString, [body objectForKey: @"source"]);
NSString* target = $castIf(NSString, [body objectForKey: @"target"]);
@@ -197,9 +205,8 @@ - (TDStatus) do_POST_all_docs: (TDDatabase*)db {
if (![self getQueryOptions: &options])
return 400;
- NSDictionary* body = [NSJSONSerialization JSONObjectWithData: _request.HTTPBody
- options: 0 error: nil];
- if (![body isKindOfClass: [NSDictionary class]])
+ NSDictionary* body = self.bodyAsDictionary;
+ if (!body)
return 400;
NSArray* docIDs = [body objectForKey: @"keys"];
if (![docIDs isKindOfClass: [NSArray class]])
@@ -213,6 +220,56 @@ - (TDStatus) do_POST_all_docs: (TDDatabase*)db {
}
+- (TDStatus) do_POST_bulk_docs: (TDDatabase*)db {
+ // http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API
+ NSDictionary* body = self.bodyAsDictionary;
+ NSArray* docs = $castIf(NSArray, [body objectForKey: @"docs"]);
+ Log(@"_bulk_docs: Got %@", body); //TEMP
+ if (!docs)
+ return 400;
+ id allObj = [body objectForKey: @"all_or_nothing"];
+ BOOL allOrNothing = (allObj && allObj != $false);
+ //BOOL noNewEdits = ([body objectForKey: @"new_edits"] == $false);
+
+ BOOL ok = NO;
+ NSMutableArray* results = [NSMutableArray arrayWithCapacity: docs.count];
+ [_db beginTransaction];
+ @try{
+ for (NSDictionary* doc in docs) {
+ NSString* docID = [doc objectForKey: @"_id"];
+ TDRevision* rev;
+ TDStatus status = [self update: db
+ docID: docID
+ body: [TDBody bodyWithProperties: doc]
+ deleting: NO
+ allowConflict: allOrNothing
+ createdRev: &rev];
+ NSDictionary* result;
+ if (status < 300) {
+ Assert(rev.revID);
+ result = $dict({@"id", rev.docID}, {@"rev", rev.revID}, {@"ok", $true});
+ } else if (allOrNothing) {
+ return status; // all_or_nothing backs out if there's any error
+ } else if (status == 403) {
+ result = $dict({@"id", docID}, {@"error", @"validation failed"});
+ } else if (status == 409) {
+ result = $dict({@"id", docID}, {@"error", @"conflict"});
+ } else {
+ return status; // abort the whole thing if something goes badly wrong
+ }
+ [results addObject: result];
+ }
+ ok = YES;
+ } @finally {
+ [_db endTransaction: ok];
+ }
+
+ Log(@"_bulk_docs: Returning %@", results); //TEMP
+ _response.bodyObject = results;
+ return 201;
+}
+
+
- (TDStatus) do_POST_compact: (TDDatabase*)db {
return [db compact];
}
@@ -405,13 +462,13 @@ - (TDStatus) do_GET: (TDDatabase*)db docID: (NSString*)docID attachment: (NSStri
- (TDStatus) update: (TDDatabase*)db
docID: (NSString*)docID
- json: (NSData*)json
+ body: (TDBody*)body
deleting: (BOOL)deleting
+ allowConflict: (BOOL)allowConflict
+ createdRev: (TDRevision**)outRev
{
- BOOL posting = (docID == nil);
- TDBody* body = json ? [TDBody bodyWithJSON: json] : nil;
-
NSString* prevRevID;
+
if (!deleting) {
deleting = $castIf(NSNumber, [body propertyForKey: @"_deleted"]).boolValue;
if (!docID) {
@@ -447,13 +504,30 @@ - (TDStatus) update: (TDDatabase*)db
rev.body = body;
TDStatus status;
- rev = [db putRevision: rev prevRevisionID: prevRevID status: &status];
+ *outRev = [db putRevision: rev prevRevisionID: prevRevID
+ allowConflict: allowConflict
+ status: &status];
+ return status;
+}
+
+
+- (TDStatus) update: (TDDatabase*)db
+ docID: (NSString*)docID
+ json: (NSData*)json
+ deleting: (BOOL)deleting
+{
+ TDBody* body = json ? [TDBody bodyWithJSON: json] : nil;
+ TDRevision* rev;
+ TDStatus status = [self update: db docID: docID body: body
+ deleting: deleting
+ allowConflict: NO
+ createdRev: &rev];
if (status < 300) {
[self setResponseEtag: rev];
if (!deleting) {
NSURL* url = _request.URL;
- if (posting)
- url = [url URLByAppendingPathComponent: docID];
+ if (!docID)
+ url = [url URLByAppendingPathComponent: rev.docID];
[_response.headers setObject: url.absoluteString forKey: @"Location"];
}
_response.bodyObject = $dict({@"ok", $true},
@@ -463,7 +537,6 @@ - (TDStatus) update: (TDDatabase*)db
return status;
}
-
- (TDStatus) do_PUT: (TDDatabase*)db docID: (NSString*)docID {
NSData* json = _request.HTTPBody;
if (!json)
View
1 Source/TDRouter.h
@@ -58,6 +58,7 @@ typedef void (^OnFinishedBlock)();
- (id) jsonQuery: (NSString*)param error: (NSError**)outError;
- (TDContentOptions) contentOptions;
- (BOOL) getQueryOptions: (struct TDQueryOptions*)options;
+@property (readonly) NSDictionary* bodyAsDictionary;
- (TDStatus) openDB;
- (void) sendResponse;
@end
View
20 Source/TDRouter.m
@@ -106,6 +106,12 @@ - (id) jsonQuery: (NSString*)param error: (NSError**)outError {
}
+- (NSDictionary*) bodyAsDictionary {
+ return $castIf(NSDictionary, [NSJSONSerialization JSONObjectWithData: _request.HTTPBody
+ options: 0 error: nil]);
+}
+
+
- (TDContentOptions) contentOptions {
TDContentOptions options = 0;
if ([self boolQuery: @"attachments"])
@@ -114,6 +120,8 @@ - (TDContentOptions) contentOptions {
options |= kTDIncludeLocalSeq;
if ([self boolQuery: @"conflicts"])
options |= kTDIncludeConflicts;
+ if ([self boolQuery: @"revs"])
+ options |= kTDIncludeRevs;
if ([self boolQuery: @"revs_info"])
options |= kTDIncludeRevsInfo;
return options;
@@ -222,11 +230,12 @@ - (void) start {
return;
}
NSString* name = [_path objectAtIndex: 1];
- if (![TDDatabase isValidDocumentID: name]) {
- _response.status = 400;
- return;
- } else if (![name hasPrefix: @"_"]) {
+ if (![name hasPrefix: @"_"]) {
// Regular document
+ if (![TDDatabase isValidDocumentID: name]) {
+ _response.status = 400;
+ return;
+ }
docID = name;
} else if ([name isEqualToString: @"_design"]) {
// "_design/____" is a document name
@@ -238,6 +247,9 @@ - (void) start {
[_path replaceObjectAtIndex: 1 withObject: docID];
[_path removeObjectAtIndex: 2];
--pathLen;
+ } else if ([name hasPrefix: @"_design/"]) {
+ // This is also a design document, just with a URL-encoded "/"
+ docID = name;
} else {
// Special document name like "_all_docs":
[message insertString: name atIndex: message.length-1]; // add to 1st component of msg

0 comments on commit 666dbca

Please sign in to comment.
Something went wrong with that request. Please try again.