all repos — litestore @ 97c1f48b5e13c7ed934a77ed0e768e1ce2559c16

A minimalist nosql document store.

Started implementeing RESTful API & HTTP Server.
h3rald h3rald@h3rald.com
Sat, 24 Jan 2015 20:08:01 +0100
commit

97c1f48b5e13c7ed934a77ed0e768e1ce2559c16

parent

ffc461c25dbfe61864d9ccf3449ce34355053bcc

7 files changed, 269 insertions(+), 161 deletions(-)

jump to
M .gitignore.gitignore

@@ -1,3 +1,3 @@

nimcache -test.ls +*.ls litestore
M cli.nimcli.nim

@@ -6,7 +6,7 @@ types

const - version = "1.0" + version* = "1.0" usage* = " LiteStore v"& version & " - Lightweight REST Document Store" & """ (c) 2015 Fabio Cevasco

@@ -24,7 +24,7 @@ """

var file = "data.ls" - port = 70700 + port = 9500 address = "0.0.0.0" operation = opRun directory = ""

@@ -64,3 +64,5 @@ settings.address = address

settings.operation = operation settings.file = file settings.directory = directory +settings.appversion = version +settings.appname = "LiteStore"
A core.nim

@@ -0,0 +1,179 @@

+import + sqlite3, + db_sqlite as db, + strutils, + os, + oids, + times, + json, + pegs, + strtabs, + base64 +import + types, + contenttypes, + queries, + utils + +# Manage Datastores + +proc createDatastore*(file:string) = + if file.fileExists: + raise newException(EDatastoreExists, "Datastore '$1' already exists." % file) + let store = db.open(file, "", "", "") + store.exec(SQL_CREATE_DOCUMENTS_TABLE) + store.exec(SQL_CREATE_SEARCHCONTENTS_TABLE) + store.exec(SQL_CREATE_TAGS_TABLE) + +proc destroyDatastore*(file:string) = + try: + file.removeFile + except: + raise newException(EDatastoreUnavailable, "Datastore '$1' cannot destroyd." % file) + +proc openDatastore*(file:string): Datastore = + if not file.fileExists: + raise newException(EDatastoreDoesNotExist, "Datastore '$1' does not exists." % file) + try: + result.db = db.open(file, "", "", "") + result.path = file + except: + raise newException(EDatastoreUnavailable, "Datastore '$1' cannot be opened." % file) + +proc closeDatastore*(store:Datastore) = + try: + db.close(store.db) + except: + raise newException(EDatastoreUnavailable, "Datastore '$1' cannot be closed." % store.path) + +# Manage Documents + +proc createDocument*(store: Datastore, id="", data = "", contenttype = "text/plain", binary = -1, searchable = 1): string = + var binary = checkIfBinary(binary, contenttype) + var data = data + if binary == 1: + data = data.encode(data.len*2) + if id == "": + result = $genOid() + else: + result = id + # Store document + store.db.exec(SQL_INSERT_DOCUMENT, result, data, contenttype, binary, searchable, getTime().getGMTime().format("yyyy-MM-dd'T'hh:mm:ss'Z'")) + if binary == 0 and searchable == 1: + # Add to search index + store.db.exec(SQL_INSERT_SEARCHCONTENT, result, data) + store.addDocumentSystemTags(result, contenttype) + return result + +proc updateDocument*(store: Datastore, id: string, data: string, contenttype = "text/plain", binary = -1, searchable = 1): int64 = + var binary = checkIfBinary(binary, contenttype) + var data = data + if binary == 1: + data = data.encode(data.len*2) + result = store.db.execAffectedRows(SQL_UPDATE_DOCUMENT, data, contenttype, binary, searchable, getTime().getGMTime().format("yyyy-MM-dd'T'hh:mm:ss'Z'"), id) + store.destroyDocumentSystemTags(id) + store.addDocumentSystemTags(id, contenttype) + store.db.exec(SQL_UPDATE_SEARCHCONTENT, data, id) + +proc destroyDocument*(store: Datastore, id: string): int64 = + result = store.db.execAffectedRows(SQL_DELETE_DOCUMENT, id) + store.db.exec(SQL_DELETE_SEARCHCONTENT, id) + store.db.exec(SQL_DELETE_DOCUMENT_TAGS, id) + +proc retrieveRawDocument*(store: Datastore, id: string, options: QueryOptions = newQueryOptions()): string = + var options = options + options.single = true + var select = prepareSelectDocumentsQuery(options) + var raw_document = store.db.getRow(select.sql, id) + if raw_document[0] == "": + return "" + else: + return $store.prepareJsonDocument(raw_document) + +proc retrieveDocument*(store: Datastore, id: string, options: QueryOptions = newQueryOptions()): tuple[data: string, contenttype: string] = + var options = options + options.single = true + var select = prepareSelectDocumentsQuery(options) + var raw_document = store.db.getRow(select.sql, id) + if raw_document[0] == "": + return (data: "", contenttype: "") + else: + if raw_document[3].parseInt == 1: + 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 = + 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) + +# Manage Tags + +proc createTag*(store: Datastore, tagid, documentid: string) = + if not tagid.match(PEG_USER_TAG): + 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): + 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 + options.single = true + var query = prepareSelectTagsQuery(options) + var raw_tag = store.db.getRow(query.sql, id) + return $(%[("id", %raw_tag[0]), ("documents", %(raw_tag[1].parseInt))]) + +proc retrieveTags*(store: Datastore, options: QueryOptions = newQueryOptions()): string = + var query = prepareSelectTagsQuery(options) + var raw_tags = store.db.getAllRows(query.sql) + var tags = newSeq[JsonNode](0) + for tag in raw_tags: + tags.add(%[("id", %tag[0]), ("documents", %(tag[1].parseInt))]) + return $(%tags) + +proc packDir*(store: Datastore, dir: string) = + if not dir.dirExists: + raise newException(EDirectoryNotFound, "Directory '$1' not found." % dir) + for f in dir.walkDirRec(): + let ext = f.splitFile.ext + var d_id = f + var d_contents = f.readFile + var d_ct = "text/plain" + if CONTENT_TYPES.hasKey(ext): + d_ct = CONTENT_TYPES[ext].replace("\"", "") + var d_binary = 0 + var d_searchable = 1 + if d_ct.isBinary: + d_binary = 1 + d_searchable = 0 + discard store.createDocument(d_id, d_contents, d_ct, d_binary, d_searchable) + store.db.exec(SQL_INSERT_TAG, "$dir:"&dir, d_id) + +proc unpackDir*(store: Datastore, dir: string) = + let docs = store.db.getAllRows(SQL_SELECT_DOCUMENTS_BY_TAG, "$dir:"&dir) + for doc in docs: + let file = doc[0] + var data: string + if doc[3].parseInt == 1: + data = doc[1].decode + else: + data = doc[1] + file.parentDir.createDir + file.writeFile(data) + +proc destroyDocumentsByTag*(store: Datastore, tag: string): int64 = + result = 0 + var ids = store.db.getAllRows(SQL_SELECT_DOCUMENT_IDS_BY_TAG, tag) + for id in ids: + result.inc(store.destroyDocument(id[0]).int) + + +
M litestore.nimlitestore.nim

@@ -11,161 +11,16 @@ strtabs,

base64 import types, - contenttypes, - queries, - utils, - cli + utils, + core, + cli, + server + +from asyncdispatch import runForever {.compile: "vendor/sqlite/libsqlite3.c".} {.passC: "-DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS".} - -# Manage Datastores - -proc createDatastore*(file:string) = - if file.fileExists: - raise newException(EDatastoreExists, "Datastore '$1' already exists." % file) - let store = db.open(file, "", "", "") - store.exec(SQL_CREATE_DOCUMENTS_TABLE) - store.exec(SQL_CREATE_SEARCHCONTENTS_TABLE) - store.exec(SQL_CREATE_TAGS_TABLE) - -proc deleteDatastore*(file:string) = - try: - file.removeFile - except: - raise newException(EDatastoreUnavailable, "Datastore '$1' cannot deleted." % file) - -proc openDatastore*(file:string): Datastore = - if not file.fileExists: - raise newException(EDatastoreDoesNotExist, "Datastore '$1' does not exists." % file) - try: - result.db = db.open(file, "", "", "") - result.path = file - except: - raise newException(EDatastoreUnavailable, "Datastore '$1' cannot be opened." % file) - -proc closeDatastore*(store:Datastore) = - try: - db.close(store.db) - except: - raise newException(EDatastoreUnavailable, "Datastore '$1' cannot be closed." % store.path) - -# Manage Documents - -proc createDocument*(store: Datastore, id="", data = "", contenttype = "text/plain", binary = -1, searchable = 1): string = - var binary = checkIfBinary(binary, contenttype) - var data = data - if binary == 1: - data = data.encode(data.len*2) - if id == "": - result = $genOid() - else: - result = id - # Store document - store.db.exec(SQL_INSERT_DOCUMENT, result, data, contenttype, binary, searchable, getTime().getGMTime().format("yyyy-MM-dd'T'hh:mm:ss'Z'")) - if binary == 0 and searchable == 1: - # Add to search index - store.db.exec(SQL_INSERT_SEARCHCONTENT, result, data) - store.addDocumentSystemTags(result, contenttype) - return result - -proc updateDocument*(store: Datastore, id: string, data: string, contenttype = "text/plain", binary = -1, searchable = 1): int64 = - var binary = checkIfBinary(binary, contenttype) - var data = data - if binary == 1: - data = data.encode(data.len*2) - result = store.db.execAffectedRows(SQL_UPDATE_DOCUMENT, data, contenttype, binary, searchable, getTime().getGMTime().format("yyyy-MM-dd'T'hh:mm:ss'Z'"), id) - store.deleteDocumentSystemTags(id) - store.addDocumentSystemTags(id, contenttype) - store.db.exec(SQL_UPDATE_SEARCHCONTENT, data, id) - -proc deleteDocument*(store: Datastore, id: string): int64 = - result = store.db.execAffectedRows(SQL_DELETE_DOCUMENT, id) - store.db.exec(SQL_DELETE_SEARCHCONTENT, id) - store.db.exec(SQL_DELETE_DOCUMENT_TAGS, id) - -proc retrieveDocument*(store: Datastore, id: string, options: QueryOptions = newQueryOptions()): string = - var options = options - options.single = true - var select = prepareSelectDocumentsQuery(options) - var raw_document = store.db.getRow(select.sql, id) - return $store.prepareJsonDocument(raw_document) - -proc retrieveDocuments*(store: Datastore, options: QueryOptions = newQueryOptions()): string = - 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) - -# Manage Tags - -proc createTag*(store: Datastore, tagid, documentid: string) = - if not tagid.match(PEG_USER_TAG): - raise newException(EInvalidTag, "Invalid Tag: $1" % tagid) - store.db.exec(SQL_INSERT_TAG, tagid, documentid) - -proc deleteTag*(store: Datastore, tagid, documentid: string): int64 = - if not tagid.match(PEG_USER_TAG): - 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 - options.single = true - var query = prepareSelectTagsQuery(options) - var raw_tag = store.db.getRow(query.sql, id) - return $(%[("id", %raw_tag[0]), ("documents", %(raw_tag[1].parseInt))]) - -proc retrieveTags*(store: Datastore, options: QueryOptions = newQueryOptions()): string = - var query = prepareSelectTagsQuery(options) - var raw_tags = store.db.getAllRows(query.sql) - var tags = newSeq[JsonNode](0) - for tag in raw_tags: - tags.add(%[("id", %tag[0]), ("documents", %(tag[1].parseInt))]) - return $(%tags) - -proc packDir*(store: Datastore, dir: string) = - if not dir.dirExists: - raise newException(EDirectoryNotFound, "Directory '$1' not found." % dir) - for f in dir.walkDirRec(): - let ext = f.splitFile.ext - var d_id = f - var d_contents = f.readFile - var d_ct = "text/plain" - if CONTENT_TYPES.hasKey(ext): - d_ct = CONTENT_TYPES[ext] - var d_binary = 0 - var d_searchable = 1 - if d_ct.isBinary: - d_binary = 1 - d_searchable = 0 - discard store.createDocument(d_id, d_contents, d_ct, d_binary, d_searchable) - store.db.exec(SQL_INSERT_TAG, "$dir:"&dir, d_id) - -proc unpackDir*(store: Datastore, dir: string) = - let docs = store.db.getAllRows(SQL_SELECT_DOCUMENTS_BY_TAG, "$dir:"&dir) - for doc in docs: - let file = doc[0] - var data: string - if doc[3].parseInt == 1: - data = doc[1].decode - else: - data = doc[1] - file.parentDir.createDir - file.writeFile(data) - -proc deleteDocumentsByTag*(store: Datastore, tag: string): int64 = - result = 0 - var ids = store.db.getAllRows(SQL_SELECT_DOCUMENT_IDS_BY_TAG, tag) - for id in ids: - result.inc(store.deleteDocument(id[0]).int) - - - - # Test when false: var file = "test.ls"

@@ -189,7 +44,7 @@ store.packDir("nimcache")

"test".createDir "test".setCurrentDir store.unpackDir("nimcache") - echo store.deleteDocumentsByTag("$dir:nimcache") + echo store.destroyDocumentsByTag("$dir:nimcache") when isMainModule: # Initialize Datastore

@@ -208,5 +63,11 @@ settings.store.packDir(settings.directory)

of opUnpack: settings.store.unpackDir(settings.directory) of opRun: - #TODO - discard + # STARTTEST + settings.file.destroyDatastore() + settings.file.createDatastore() + settings.store = settings.file.openDatastore() + settings.store.packDir("nimcache") + # ENDTEST + settings.serve + runForever()
A server.nim

@@ -0,0 +1,59 @@

+import asynchttpserver, asyncdispatch, times, strutils, pegs +from strtabs import StringTableRef, newStringTable +import types, core + + +const + CT_JSON = {"Content-type": "application/json"} + +proc getReqInfo(req): string = + return $getLocalTime(getTime()) & " - " & req.hostname & " " & req.reqMethod & " " & req.url.path + +proc handleCtrlC() {.noconv.} = + echo "\nExiting..." + quit() + +proc resDocumentNotFound(id): Response = + result.content = """{"code": 404, "message": "Document '$1' not found."}""" % id + result.code = Http404 + result.headers = CT_JSON.newStringTable + +proc getRawDocument(settings: Settings, id: string): Response = + let doc = settings.store.retrieveRawDocument(id) + result.headers = CT_JSON.newStringTable + if doc == "": + result = resDocumentNotFound(id) + else: + result.content = doc + result.code = Http200 + +proc route(req: Request, settings: Settings): Response = + case req.reqMethod: + of "GET": + var matches = @[""] + if req.url.path.find(peg"""^\/docs\/? {(.*)}""", matches) != -1: + if matches[0] != "": + # Retrieve a single document + return settings.getRawDocument(matches[0]) + else: + result = resDocumentNotFound("-") # TODO CHANGE + else: + result.code = Http400 + result.content = """{"code": 400, "message": "Bad request: $1"}""" % req.url.path + result.headers = CT_JSON.newStringTable + else: + result.content = """{"code": 501, "message": "Method $1 not implemented."}""" % req.reqMethod + result.headers = CT_JSON.newStringTable + result.code = Http501 + +setControlCHook(handleCtrlC) + +proc serve*(settings: Settings) = + var server = newAsyncHttpServer() + proc handleHttpRequest(req: Request): Future[void] {.async.} = + echo getReqInfo(req) + let res = req.route(settings) + await req.respond(res.code, res.content, res.headers) + echo settings.appname, " v", settings.appversion, " started on ", settings.address, ":", settings.port, "." + asyncCheck server.serve(settings.port.Port, handleHttpRequest, settings.address) +
M types.nimtypes.nim

@@ -1,4 +1,6 @@

-import db_sqlite +import db_sqlite +from asynchttpserver import HttpCode +from strtabs import StringTableRef, newStringTable type EDatastoreExists* = object of Exception

@@ -28,6 +30,12 @@ port*: int

operation*: Operation directory*: string file*: string + appname*: string + appversion*: string + Response* = tuple[ + code: HttpCode, + content: string, + headers: StringTableRef] proc newQueryOptions*(): QueryOptions = return QueryOptions(single: false, limit: 0, orderby: "", tags: "", search: "")
M utils.nimutils.nim

@@ -37,7 +37,7 @@ if options.search.len > 0:

result = result & "FROM documents, searchcontents " result = result & "WHERE documents.id = searchcontents.document_id " else: - result = result & "FROM documents " + result = result & "FROM documents WHERE 1=1 " if options.single: result = result & "AND id = ?" if options.tags.len > 0:

@@ -48,7 +48,6 @@ if options.orderby.validOrderBy():

result = result & "ORDER BY " & options.orderby & " " if options.limit > 0: result = result & "LIMIT " & $options.limit & " " - echo result proc prepareSelectTagsQuery*(options: QueryOptions): string = result = "SELECT tag_id, COUNT(document_ID) "

@@ -91,7 +90,7 @@ tags.add "$format:text"

for tag in tags: store.db.exec(SQL_INSERT_TAG, tag, docid) -proc deleteDocumentSystemTags*(store: Datastore, docid) = +proc destroyDocumentSystemTags*(store: Datastore, docid) = store.db.exec(SQL_DELETE_DOCUMENT_SYSTEM_TAGS, docid) proc error*(code, msg) =