Implemented read-only mode; response metadata, method override.
@@ -64,6 +64,44 @@ else:
discard return cb(req, LS, id) +proc applyPatchOperation(tags: var seq[string], op: string, path: string, value: string): bool = + var matches = @[""] + if path.find(peg"^\/tags\/{\d+}$", matches) == -1: + raise newException(EInvalidRequest, "cannot patch path '$1'" % path) + let index = matches[0].parseInt + case op: + of "remove": + let tag = tags[index] + if not tag.startsWith("$"): + system.delete(tags, index) + else: + raise newException(EInvalidRequest, "Cannot remove system tag: $1" % tag) + of "add": + if value.match(PEG_USER_TAG): + tags.insert(value, index) + else: + if value.strip == "": + raise newException(EInvalidRequest, "tag not specified." % value) + else: + raise newException(EInvalidRequest, "invalid tag: $1" % value) + of "replace": + if value.match(PEG_USER_TAG): + if tags[index].startsWith("$"): + raise newException(EInvalidRequest, "Cannot replace system tag: $1" % tags[index]) + else: + tags[index] = value + else: + if value.strip == "": + raise newException(EInvalidRequest, "tag not specified." % value) + else: + raise newException(EInvalidRequest, "invalid tag: $1" % value) + of "test": + if tags[index] != value: + return false + else: + raise newException(EInvalidRequest, "invalid operation: $1" % op) + return true + # Low level procs proc getRawDocument(LS: LiteStore, id: string, options = newQueryOptions()): Response =@@ -100,13 +138,29 @@ result.code = Http204
except: result = resError(Http500, "Unable to delete document '$1'" % id) -proc getRawDocuments(LS: LiteStore, options = newQueryOptions()): Response = - let docs = LS.store.retrieveRawDocuments(options) - if docs == "[]": +proc getRawDocuments(LS: LiteStore, options: QueryOptions = newQueryOptions()): Response = + var options = options + let docs = LS.store.retrieveRawDocuments(options) + var orig_limit = options.limit + options.limit = 0 + options.select = "COUNT(id)" + let total = LS.store.retrieveRawDocuments(options)[0].num + if docs == %"[]": result = resError(Http404, "No documents found.") else: + var content = newJObject() + if options.search != "": + content["search"] = %(options.search.decodeURL) + if options.tags != "": + content["tags"] = newJArray() + for tag in options.tags.decodeURL.split(","): + content["tags"].add(%tag) + if orig_limit > 0: + content["limit"] = %orig_limit + content["total"] = %total + content["results"] = docs result.headers = ctJsonHeader() - result.content = docs + result.content = $content result.code = Http200 proc postDocument(LS: LiteStore, body: string, ct: string): Response =@@ -145,17 +199,59 @@ result = resError(Http500, "Unable to update document '$1'." % id)
except: result = resError(Http500, "Unable to update document '$1'." % id) +proc patchDocument(LS: LiteStore, id: string, body: string): Response = + var apply = true + let jbody = body.parseJson + if jbody.kind != JArray: + return resError(Http400, "Bad request: PATCH request body is not an array.") + var options = newQueryOptions() + options.select = "id, content_type, binary, searchable, created, modified" + let doc = LS.store.retrieveRawDocument(id, options) + if doc == "": + return resDocumentNotFound(id) + let jdoc = doc.parseJson + var tags = newSeq[string](0) + for tag in jdoc["tags"].items: + tags.add(tag.str) + var c = 1 + for item in jbody.items: + if item.hasKey("op") and item.hasKey("path"): + if not item.hasKey("value"): + item["value"] = %"" + try: + apply = applyPatchOperation(tags, item["op"].str, item["path"].str, item["value"].str) + except: + return resError(Http400, "Bad request - $1" % getCurrentExceptionMsg()) + else: + return resError(Http400, "Bad request: patch operation #$1 is malformed." % $c) + c.inc + if apply: + try: + for t1 in jdoc["tags"].items: + discard LS.store.destroyTag(t1.str, id, true) + for t2 in tags: + LS.store.createTag(t2, id, true) + except: + return resError(Http500, "Unable to patch document '$1' - $2" % [id, getCurrentExceptionMsg()]) + return LS.getRawDocument(id) + # Main routing proc options(req: Request, LS: LiteStore, id = ""): Response = if id != "": result.code = Http204 result.content = "" - result.headers = {"Allow": "HEAD,GET,PUT,PATCH,DELETE"}.newStringTable + if LS.readonly: + result.headers = {"Allow": "HEAD,GET"}.newStringTable + else: + result.headers = {"Allow": "HEAD,GET,PUT,PATCH,DELETE"}.newStringTable else: result.code = Http204 result.content = "" - result.headers = {"Allow": "HEAD,GET,POST"}.newStringTable + if LS.readonly: + result.headers = {"Allow": "HEAD,GET"}.newStringTable + else: + result.headers = {"Allow": "HEAD,GET,POST"}.newStringTable proc head(req: Request, LS: LiteStore, id = ""): Response = var options = newQueryOptions()@@ -207,13 +303,28 @@ return LS.deleteDocument(id)
else: return resError(Http400, "Bad request: document ID must be specified in DELETE requests.") +proc patch(req: Request, LS: LiteStore, id = ""): Response = + if id != "": + return LS.patchDocument(id, req.body) + else: + return resError(Http400, "Bad request: document ID must be specified in PATCH requests.") + proc route*(req: Request, LS: LiteStore, id = ""): Response = - case req.reqMethod: + var reqMethod = req.reqMethod + if req.headers.hasKey("X-HTTP-Method-Override"): + reqMethod = req.headers["X-HTTP-Method-Override"] + case reqMethod.toUpper: of "POST": + if LS.readonly: + return resError(Http405, "Method not allowed: $1" % req.reqMethod) return validate(req, LS, id, post) of "PUT": + if LS.readonly: + return resError(Http405, "Method not allowed: $1" % req.reqMethod) return validate(req, LS, id, put) of "DELETE": + if LS.readonly: + return resError(Http405, "Method not allowed: $1" % req.reqMethod) return validate(req, LS, id, delete) of "HEAD": return validate(req, LS, id, head)@@ -221,5 +332,9 @@ of "OPTIONS":
return validate(req, LS, id, options) of "GET": return validate(req, LS, id, get) + of "PATCH": + if LS.readonly: + return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return validate(req, LS, id, patch) else: return resError(Http405, "Method not allowed: $1" % req.reqMethod)
@@ -28,6 +28,7 @@ port = 9500
address = "0.0.0.0" operation = opRun directory = "" + readonly = false for kind, key, val in getOpt():@@ -50,6 +51,8 @@ quit(0)
of "help", "h": echo usage quit(0) + of "readonly", "r": + readonly = true else: discard of cmdArgument:@@ -65,4 +68,5 @@ LS.operation = operation
LS.file = file LS.directory = directory LS.appversion = version +LS.readonly = readonly LS.appname = "LiteStore"
@@ -4,7 +4,6 @@ db_sqlite as db,
strutils, os, oids, - times, json, pegs, strtabs,@@ -69,7 +68,7 @@ data = data.encode(data.len*2)
if id == "": id = $genOid() # Store document - store.db.exec(SQL_INSERT_DOCUMENT, id, data, contenttype, binary, searchable, getTime().getGMTime().format("yyyy-MM-dd'T'hh:mm:ss'Z'")) + store.db.exec(SQL_INSERT_DOCUMENT, id, data, contenttype, binary, searchable, currentTime()) if binary <= 0 and searchable >= 0: # Add to search index store.db.exec(SQL_INSERT_SEARCHCONTENT, id, data)@@ -82,7 +81,7 @@ var binary = checkIfBinary(binary, contenttype)
var data = data if binary == 1: data = data.encode(data.len*2) - var res = store.db.execAffectedRows(SQL_UPDATE_DOCUMENT, data, contenttype, binary, searchable, getTime().getGMTime().format("yyyy-MM-dd'T'hh:mm:ss'Z'"), id) + var res = store.db.execAffectedRows(SQL_UPDATE_DOCUMENT, data, contenttype, binary, searchable, currentTime(), id) if res > 0: store.destroyDocumentSystemTags(id) store.addDocumentSystemTags(id, contenttype)@@ -90,6 +89,9 @@ store.db.exec(SQL_UPDATE_SEARCHCONTENT, data, id)
return $store.retrieveRawDocument(id) else: return "" + +proc setDocumentModified*(store: Datastore, id: string): string = + store.db.exec(SQL_SET_DOCUMENT_MODIFIED, id, currentTime()) proc destroyDocument*(store: Datastore, id: string): int64 = result = store.db.execAffectedRows(SQL_DELETE_DOCUMENT, id)@@ -109,25 +111,27 @@ return (data: raw_document[1].decode, contenttype: raw_document[2])
else: return (data: raw_document[1], contenttype: raw_document[2]) -proc retrieveRawDocuments*(store: Datastore, options: QueryOptions = newQueryOptions()): string = +proc retrieveRawDocuments*(store: Datastore, options: QueryOptions = newQueryOptions()): JsonNode = var select = prepareSelectDocumentsQuery(options) var raw_documents = store.db.getAllRows(select.sql) var documents = newSeq[JsonNode](0) for doc in raw_documents: documents.add store.prepareJsonDocument(doc) - return $(%documents) + return %documents # Manage Tags -proc createTag*(store: Datastore, tagid, documentid: string) = - if not tagid.match(PEG_USER_TAG): +proc createTag*(store: Datastore, tagid, documentid: string, system=false) = + if tagid.match(PEG_USER_TAG) or system and tagid.match(PEG_TAG): + store.db.exec(SQL_INSERT_TAG, tagid, documentid) + else: raise newException(EInvalidTag, "Invalid Tag: $1" % tagid) - store.db.exec(SQL_INSERT_TAG, tagid, documentid) -proc destroyTag*(store: Datastore, tagid, documentid: string): int64 = - if not tagid.match(PEG_USER_TAG): +proc destroyTag*(store: Datastore, tagid, documentid: string, system=false): int64 = + if tagid.match(PEG_USER_TAG) or system and tagid.match(PEG_TAG): + return store.db.execAffectedRows(SQL_DELETE_TAG, tagid, documentid) + else: raise newException(EInvalidTag, "Invalid Tag: $1" % tagid) - return store.db.execAffectedRows(SQL_DELETE_TAG, documentid, tagid) proc retrieveTag*(store: Datastore, id: string, options: QueryOptions = newQueryOptions()): string = var options = options
@@ -47,6 +47,12 @@ modified = ?
WHERE id = ? """ +const SQL_SET_DOCUMENT_MODIFIED* = sql""" +UPDATE documents +SET modified = ? +WHERE id = ? +""" + const SQL_DELETE_DOCUMENT* = sql""" DELETE FROM documents WHERE id = ?@@ -60,7 +66,7 @@ """
const SQL_DELETE_TAG* = sql""" DELETE FROM tags -WHERE document_id = ? AND tag_id = ? +WHERE tag_id = ? AND document_id = ? """ const SQL_DELETE_DOCUMENT_TAGS* = sql"""
@@ -1,4 +1,4 @@
-import json, db_sqlite, strutils, pegs, asyncdispatch, asynchttpserver2 +import json, db_sqlite, strutils, pegs, asyncdispatch, asynchttpserver2, times import types, queries, contenttypes proc dbQuote*(s: string): string =@@ -7,6 +7,9 @@ for c in items(s):
if c == '\'': add(result, "''") else: add(result, c) add(result, '\'') + +proc currentTime*(): string = + return getTime().getGMTime().format("yyyy-MM-dd'T'hh:mm:ss'Z'") proc selectDocumentsByTags(tags: string): string = var select_tagged = "SELECT document_id FROM tags WHERE tag_id = \""@@ -50,7 +53,10 @@ var raw_tags = store.db.getAllRows(SQL_SELECT_DOCUMENT_TAGS, doc[0])
var tags = newSeq[JsonNode](0) for tag in raw_tags: tags.add(%($(tag[0]))) - if doc.len > 6: + if doc.len == 1: + # COUNT(id) + return %(doc[0].parseInt) + elif doc.len > 6: return % [("id", %doc[0]), ("data", %doc[1]), ("created", %doc[5]),