arbase
Advanced tools
Comparing version 0.1.5 to 0.2.0
{ | ||
"name": "arbase", | ||
"version": "0.1.5", | ||
"version": "0.2.0", | ||
"description": "Arbase is a tool to create object-based APIs on top of arweave in mere minutes", | ||
@@ -31,3 +31,3 @@ "main": "src/index.js", | ||
"@hapi/joi": "^16.1.7", | ||
"arlang": "^0.1.3", | ||
"arlang": "^0.1.4", | ||
"arweave": "^1.4.1", | ||
@@ -34,0 +34,0 @@ "base-x": "^3.0.7", |
@@ -7,3 +7,3 @@ 'use strict' | ||
const { decodeAndValidate, decodeAndValidateList, ListEventType, decodeTxData } = require('./process') | ||
const { decodeAndValidate, decodeTxData } = require('./process') | ||
@@ -35,73 +35,233 @@ const queue = require('../queue')() | ||
function validateListEntry (entry, listEntry, {data, tags}) { | ||
// TODO: return false if invalid | ||
return {data, tags} | ||
const typeRE = /(@([a-z0-9]+)\/)?([a-z0-9]+)/i | ||
function parseType (str) { | ||
const [,, ns, name] = str.match(typeRE) | ||
return {ns, name} | ||
} | ||
function joinListOplog (data, idMap, tx) { | ||
// TODO: add | ||
const lexer = require('arlang/src/lexer') | ||
/* | ||
function parser (query, config = {}, e) { | ||
const tokens = lexer(query) | ||
op is either append or delete | ||
target is a blockId | ||
let block = 'select' | ||
*/ | ||
let i = 0 | ||
switch (data.type) { | ||
case ListEventType.APPEND: { | ||
idMap[data.target] = data.push(data.target) | ||
break | ||
query = { /* tags: {}, */ order: [] } | ||
function expect (type, value) { | ||
const token = tokens[i] | ||
if (token.type !== type) throw new TypeError('Expected ' + type + ', got ' + token.type) | ||
if (value) { | ||
if (token.value !== value) throw new TypeError('Expected ' + value + ', got ' + token.value) | ||
} | ||
case ListEventType.DELETE: { | ||
delete data[idMap[data.target]] | ||
break | ||
} | ||
function assume (type, value) { | ||
const token = tokens[i] | ||
return (token.type === type && ((!value) || (token.value === value))) | ||
} | ||
// TODO: handle deepRefs such as x.y | ||
while (tokens[i]) { | ||
switch (block) { | ||
case 'select': { | ||
expect('literal', 'select') | ||
i++ | ||
block = 'single' | ||
break | ||
} | ||
case 'single': { | ||
if (assume('literal', 'single')) { | ||
query.single = true | ||
i++ | ||
} | ||
block = 'type' | ||
break | ||
} | ||
case 'type': { | ||
expect('literal') | ||
query.type = parseType(tokens[i].value) | ||
query.typeObj = e.entry[query.type.ns || null][query.type.name] | ||
block = 'where' | ||
i++ | ||
break | ||
} | ||
case 'where': { | ||
if (assume('literal', 'where')) { | ||
block = 'whereInner' | ||
i++ | ||
} else { | ||
block = 'order' | ||
} | ||
break | ||
} | ||
case 'whereInner': { | ||
if (assume('literal', 'order')) { | ||
block = 'orderInner' | ||
i++ | ||
break | ||
} | ||
// TODO v2: expect "=" eq, "<" lt, ">" gt, "<=" lteq, ">=" gteq, "LIKE" (like) | ||
// TODO v1: expect "=" eq, "!=" not eq | ||
// TODO v0: just arlang query | ||
// query is string | ||
query.arql = arlang(tokens[i].value, {lang: config.arqlLang || 'sym', params: config.params}) | ||
i++ | ||
break | ||
} | ||
case 'where2Inner': { | ||
// where query on data AFTER processing | ||
/* | ||
// TODO: expect literal or string | ||
const tag = token.value | ||
i++ | ||
const op = token.value | ||
i++ | ||
// expect literal, integer, string | ||
const comp = token.value | ||
i++ | ||
out.tags[tag] = { | ||
op, | ||
comp | ||
} */ | ||
break | ||
} | ||
case 'order': { | ||
if (assume('literal', 'order')) { | ||
i++ | ||
expect('literal', 'by') | ||
i++ | ||
block = 'orderInner' | ||
} else { | ||
block = 'eof' | ||
} | ||
break | ||
} | ||
case 'orderInner': { | ||
// TODO: expect literal or string | ||
const key = tokens[i].value | ||
i++ | ||
// TODO: expect literal val asc/desc | ||
const type = tokens[i].value | ||
i++ | ||
query.order.push([key, type]) | ||
break | ||
} | ||
case 'eof': { | ||
expect('(no further tokens expected)') | ||
break | ||
} | ||
default: { | ||
throw new TypeError(block) | ||
} | ||
} | ||
default: { | ||
throw new TypeError(data.op) | ||
} | ||
} | ||
return query | ||
} | ||
module.exports = (arweave) => { | ||
/* const OPs = { | ||
eq: (val, comp) => val === comp, | ||
lt: (val, comp) => val > comp, | ||
gt: (val, comp) => val < comp, | ||
lteq: (val, comp) => val >= comp, | ||
gteq: (val, comp) => val <= comp, | ||
like: (val, comp) => null // TODO: check for strings via substr, for numbers via compare | ||
} */ | ||
module.exports = (arweave, e) => { | ||
async function fetchTransaction (id) { | ||
return decodeTxData(await arweave.transactions.get(id)) | ||
const tx = await arweave.transactions.get(id) | ||
const tags = {} | ||
tx.get('tags').forEach(tag => { | ||
const key = tag.get('name', {decode: true, string: true}) | ||
const value = tag.get('value', {decode: true, string: true}) | ||
tags[key] = value | ||
}) | ||
const time = 0 // TODO: fix this because time blargh | ||
return { | ||
data: decodeTxData(tx), | ||
tags, | ||
time, | ||
owner: tx.owner, // TODO: check if works | ||
id | ||
} | ||
} | ||
const f = { | ||
list: async (entry, listEntry, id, parse) => { | ||
let data = [] | ||
let idMap = {} | ||
const {data: txs, live} = await arweave.arql($arql('& (= block $1) (= child $2)', id, String(listEntry.id))) | ||
// SELECT topic WHERE parent = 'someTopic' -> get topic ORDER BY createdAt | ||
// SELECT SINGLE topic WHERE rid = 'rid' -> get single | ||
// single indicates it should just yield a single element (validation is whether or not rid exists, so only do ) | ||
// | ||
// TODO: all tags must be the same for all changes, otherwise querying breaks horribly | ||
// TODO: possible make tags hex but store binary? | ||
// TODO: tags should be processed in the same way as attributes, just instead their "tags" named and no modify perms | ||
// TODO: lists are trash now | ||
// TODO: acls would need a "what is our previous element" reference helper, to say that for ex "p" is previous element tag and then read that | ||
// TODO: better queuing | ||
queue.init(id, 3, 50) | ||
// TODO: make one where query, but seperate tags into arql and data into where2 | ||
const txLog = txs.reverse().map(() => queue(id, async () => | ||
decodeAndValidateList(await fetchTransaction(id)))) | ||
// TODO: flatened ACLs (isHirarchy$rootId, hirarchyPosition=$actualId) | ||
// board $id -> topic $id | ||
// board acl=isHirarchy$board | ||
// topic acl=isHirarchy$board, isHirarchy$board | ||
// tree is determined by ACL parent resolution | ||
// acl resolution would ask for topic acl using isHirarchy$root and then filter out valid board acl after parsing all using hirarchy=$topicId | ||
for (let i = txLog.length; i > -1; i--) { | ||
const tx = await txLog[i] | ||
if (tx) { | ||
data = joinListOplog(data, idMap, tx) | ||
} | ||
} | ||
// aclv2: | ||
// or(equals(h, $board), equals(h, $topic), equals(h, $subtopic)) - this would give us ACLs for the entire hirarchy, now just need efficient way to fetch hirarchy | ||
if (!parse) { | ||
return {data: data.filter(Boolean), live} | ||
} | ||
// TODO: should we instead enforce parent as a tag and make it a tree?! | ||
const { offset, limit } = parse | ||
query: async function query (query, _qconf) { | ||
query = typeof query === 'string' ? parser(query, _qconf, e) : query | ||
// TODO: use id as cursor | ||
const total = data.filter(Boolean).length | ||
const range = data.filter(Boolean).reverse().slice(offset, offset + limit) | ||
const {typeObj: entry} = query | ||
return { | ||
data: range, | ||
total, | ||
live | ||
const el = {} | ||
const {data: txs, live} = await arweave.arql(query.arql) | ||
for (let i = txs.length; i < txs.length; i--) { | ||
const {data, tags, time, owner, id} = await fetchTransaction(txs[i]) | ||
// TODO: acl | ||
if (tags.a === 'c') { | ||
el[tags.i] = await decodeAndValidate(entry, data, false) | ||
el[tags.i].id = tags.i | ||
el[tags.i].tx = [id] | ||
el[tags.i].createdAt = time | ||
el[tags.i].owner = owner | ||
} else if (tags.a === 'e') { | ||
el[tags.i] = joinOplog(el[tags.i], await decodeAndValidate(entry, data, true)) // joinOplog(obj, await decodeAndValidate(entry, data, true)) | ||
el[tags.i].tx.push(id) | ||
el[tags.i].modifiedAt = time | ||
} else if (tags.a === 'd') { | ||
delete el[tags.i] | ||
// TODO: instead set .deletedAt and do soft delete? (we could also do a='r' to restore) | ||
} | ||
} | ||
// TODO: handle single flag | ||
return {data: el, live} | ||
}, | ||
@@ -133,9 +293,4 @@ entry: async function fetchEntry (entry, id) { | ||
const txLog = txs.reverse().map(() => queue(id, async () => { | ||
const fetched = await fetchTransaction(id) | ||
return validateEntry(entry, fetched, false) | ||
})) | ||
for (let i = txLog.length; i > -1; i--) { | ||
const tx = await txLog[i] | ||
for (let i = txs.length; i > -1; i--) { | ||
const tx = await txs[i] | ||
if (tx) { | ||
@@ -147,2 +302,4 @@ const data = await fetchTransaction(tx) | ||
obj.id = id | ||
return {data: obj, live} | ||
@@ -149,0 +306,0 @@ } |
'use strict' | ||
module.exports = (arweave) => { | ||
module.exports = (arweave, e) => { | ||
return { | ||
read: require('./fetch')(arweave), | ||
write: require('./update')(arweave) | ||
read: require('./fetch')(arweave, e), | ||
write: require('./update')(arweave, e) | ||
} | ||
} |
@@ -5,2 +5,4 @@ 'use strict' | ||
// TODO: attempt compression using $algo and use multiprefix, then uses that if smaller in total | ||
const Joi = require('@hapi/joi') | ||
@@ -11,17 +13,2 @@ | ||
const protons = require('protons') | ||
const { ListEvent, ListEventType } = protons(` | ||
enum ListEventType { | ||
APPEND = 1; | ||
DELETE = 2; | ||
} | ||
message ListEvent { | ||
ListEventType type = 1; | ||
bytes blockId = 2; | ||
} | ||
`) | ||
async function decodeAndValidate (entry, data, half) { | ||
@@ -77,33 +64,2 @@ const decoded = entry.message.decode(data) | ||
const listValidator = Joi.object({ | ||
type: Joi.number().integer().required(), | ||
blockId: Joi.string().regex(/[a-zA-Z0-9_-]{43}/).required() | ||
}) | ||
async function decodeAndValidateList (data) { | ||
const decoded = ListEvent.decode(data) | ||
decoded.blockId = b.encode(decoded.blockId) | ||
const {error, value} = listValidator.validate(data) | ||
if (error) { | ||
throw error | ||
} | ||
return value | ||
} | ||
async function validateAndEncodeList (data) { | ||
const {error, value} = listValidator.validate(data) | ||
value.blockId = b.decode(value.blockId) | ||
if (error) { | ||
throw error | ||
} | ||
return ListEvent.encode(value) | ||
} | ||
function decodeTxData (tx) { | ||
@@ -121,6 +77,2 @@ return Buffer.from(tx.get('data', {decode: true})) | ||
decodeAndValidateList, | ||
validateAndEncodeList, | ||
ListEventType, | ||
decodeTxData, | ||
@@ -127,0 +79,0 @@ encodeTxData, |
'use strict' | ||
const { validateAndEncode, validateAndEncodeList, ListEventType, encodeTxData } = require('./process') | ||
const crypto = require('crypto') | ||
const getRandomID = () => crypto.randomBytes(16).toString('hex') | ||
const { validateAndEncode, encodeTxData } = require('./process') | ||
async function createTx (data, arweave) { | ||
@@ -13,20 +16,25 @@ return arweave.createTransaction({ | ||
async function entryCreate (arweave, entry, val) { | ||
async function entryCreate (arweave, entry, tags, val) { | ||
const tx = await createTx(await validateAndEncode(entry, val), arweave) | ||
const rid = getRandomID() | ||
for (const tag in tags) { // eslint-disable-line guard-for-in | ||
tx.addTag(tag, tags[tag]) | ||
} | ||
tx.addTag('i', rid) | ||
tx.addTag('a', 'c') | ||
return tx | ||
} | ||
async function entryModify (arweave, entry, id, diff) { | ||
async function entryModify (arweave, entry, rid, tags, diff) { | ||
const tx = await createTx(await validateAndEncode(entry, diff, true), arweave) | ||
tx.addTag('block', id) | ||
tx.addTag('child', '#') | ||
return tx | ||
} | ||
for (const tag in tags) { // eslint-disable-line guard-for-in | ||
tx.addTag(tag, tags[tag]) | ||
} | ||
async function entryDelete (arweave, entry, id, diff) { | ||
const tx = await createTx(/* TODO */ '', arweave) | ||
tx.addTag('block', id) | ||
tx.addTag('child', '#') | ||
tx.addTag('i', rid) | ||
tx.addTag('a', 'e') | ||
@@ -36,15 +44,11 @@ return tx | ||
// TODO: rewrite below | ||
async function listAppend (arweave, entry, listEntry, id, blockId) { | ||
const tx = await createTx(await validateAndEncode({ type: ListEventType.APPEND, blockId }), arweave) | ||
tx.addTag('block', id) | ||
tx.addTag('child', String(listEntry.id)) | ||
async function entryDelete (arweave, entry, rid, tags) { | ||
const tx = await createTx(Buffer.from(''), arweave) // TODO: add contents | ||
return tx | ||
} | ||
for (const tag in tags) { // eslint-disable-line guard-for-in | ||
tx.addTag(tag, tags[tag]) | ||
} | ||
async function listRemove (arweave, entry, listEntry, id, blockId) { | ||
const tx = await createTx(await validateAndEncode({ type: ListEventType.DELETE, blockId }), arweave) | ||
tx.addTag('block', id) | ||
tx.addTag('child', String(listEntry.id)) | ||
tx.addTag('i', rid) | ||
tx.addTag('a', 'd') | ||
@@ -54,9 +58,7 @@ return tx | ||
module.exports = (arweave) => { | ||
module.exports = (arweave, e) => { | ||
const out = { | ||
entryCreate, | ||
entryModify, | ||
entryDelete, | ||
listAppend, | ||
listRemove | ||
entryDelete | ||
} | ||
@@ -63,0 +65,0 @@ |
'use strict' | ||
module.exports = { | ||
file: require('./file'), | ||
block: require('./block'), | ||
number: require('./number'), | ||
string: require('./string') | ||
string: require('./string'), | ||
jsonb: require('./jsonb') | ||
} |
42845
24
1116
Updatedarlang@^0.1.4