Comparing version 1.0.1 to 1.1.0
@@ -14,2 +14,8 @@ declare module "ar-wrapper" { | ||
interface FetchSettingsI { | ||
maxRetries: number | ||
verifiedOnly: boolean | ||
maxResults: number | ||
} | ||
export interface BlockDocument { | ||
@@ -30,5 +36,7 @@ name: string | ||
export const DEFAULT_OPTIONS: OptionsI | ||
interface Serializable { | ||
toString: (any) => string | ||
} | ||
export class Document { | ||
export class Document<T = string> { | ||
txID: string | ||
@@ -40,3 +48,3 @@ client: ArweaveClient | ||
name: string | ||
content: any | ||
content: T | ||
version: number | ||
@@ -62,5 +70,7 @@ tags: Record<string, string> | ||
pollForConfirmation(txId: string, maxRetries?: number): Promise<BlockStatusI> | ||
getDocumentByName(name: string, version?: number, maxRetries?: number, verifiedOnly?: boolean): Promise<Document> | ||
getDocumentByTxId(txId: string, maxRetries?: number, verifiedOnly?: boolean): Promise<Document> | ||
executeQuery(names: string[], versions: number[], userTags: Record<string, string>, userOptions?: Partial<FetchSettingsI>) | ||
getDocumentsByName(name: string, version?: number, tags?: Record<string, string>, options?: Partial<FetchSettingsI>): Promise<Document[]> | ||
getDocumentsByTags(tags: Record<string, string>, options?: Partial<FetchSettingsI>): Promise<Document[]> | ||
getDocumentByTxId(txId: string, userOptions?: Partial<FetchSettingsI>): Promise<Document> | ||
} | ||
} |
149
index.js
@@ -6,3 +6,3 @@ const ArweaveLib = require('arweave') | ||
const DEFAULT_OPTIONS = { | ||
const DEFAULT_ARWEAVE_OPTIONS = { | ||
host: 'arweave.net', | ||
@@ -15,2 +15,8 @@ port: 443, | ||
const DEFAULT_FETCH_OPTIONS = { | ||
maxRetries: 10, | ||
verifiedOnly: true, | ||
maxResults: 25, | ||
} | ||
// A single managed document containing arbitrary content. | ||
@@ -80,2 +86,3 @@ // Should not be constructed manually, do this through the `ArweaveClient` | ||
const NAME = "DOC_NAME" | ||
const META = "DOC_META" | ||
@@ -98,3 +105,3 @@ // Thin wrapper client around Arweave for versioned document/data management. | ||
// Options are identical to the ones supported by the official `arweave-js` library. | ||
constructor(adminAddress, keyFile, cacheSize = 500, options = DEFAULT_OPTIONS) { | ||
constructor(adminAddress, keyFile, cacheSize = 500, options = DEFAULT_ARWEAVE_OPTIONS) { | ||
this.#key = JSON.parse(keyFile) | ||
@@ -116,2 +123,7 @@ this.adminAddr = adminAddress | ||
// add user defined metadata | ||
Object.entries(doc.tags).forEach(([tag, content]) => { | ||
tx.addTag(`${META}_${tag}`, content) | ||
}) | ||
// sign + send tx | ||
@@ -129,3 +141,3 @@ await this.client.transactions.sign(tx, this.#key) | ||
doc.posted = true | ||
this.cache.set(doc.name, doc) | ||
this.cache.set(doc.txID, doc) | ||
return doc | ||
@@ -136,4 +148,4 @@ } | ||
// Optionally define desired version to match against. | ||
isCached(documentName, desiredVersion) { | ||
const inCache = this.cache.has(documentName) | ||
isCached(txId, desiredVersion) { | ||
const inCache = this.cache.has(txId) | ||
if (!inCache) { | ||
@@ -143,3 +155,3 @@ return false | ||
const cached = this.cache.get(documentName) | ||
const cached = this.cache.get(txId) | ||
const versionMatch = desiredVersion !== undefined ? cached.version === desiredVersion : true | ||
@@ -159,3 +171,3 @@ return cached.posted && versionMatch | ||
// check if cache has latest version of document | ||
if (this.isCached(document.name, document.version)) { | ||
if (this.isCached(document.txID, document.version)) { | ||
return document | ||
@@ -166,3 +178,2 @@ } | ||
await this.#insert(document) | ||
this.cache.set(document.name, document) | ||
return document | ||
@@ -177,2 +188,6 @@ } | ||
if (this.cache.has(txId)) { | ||
return true | ||
} | ||
return await backOff(async () => { | ||
@@ -193,8 +208,17 @@ const txStatus = await this.client.transactions.getStatus(txId) | ||
// all submitted TXs (including ones from non-admin wallet accounts) | ||
#queryBuilder(names, versions, verifiedOnly = true) { | ||
const tags = [`{ | ||
name: "${NAME}", | ||
values: ${JSON.stringify(names)}, | ||
}`] | ||
#queryBuilder(names, versions, userTags, verifiedOnly = true, cursor = undefined) { | ||
// parse use defined tags | ||
const tags = Object.entries(userTags).map(([k, v]) => `{ | ||
name: "${META}_${k}", | ||
values: ["${v}"] | ||
}`) | ||
// add name tag | ||
if (names.length > 0) { | ||
tags.push(`{ | ||
name: "${NAME}", | ||
values: ${JSON.stringify(names)}, | ||
}`) | ||
} | ||
// versions is an optional field | ||
@@ -208,3 +232,2 @@ if (versions.length > 0) { | ||
// TODO: handle pagination/cursor here | ||
return { | ||
@@ -215,5 +238,7 @@ query: ` | ||
tags: [${tags.join(",")}], | ||
${verifiedOnly ? `owners: ["${this.adminAddr}"]` : ""} | ||
${verifiedOnly ? `owners: ["${this.adminAddr}"],` : ""} | ||
${cursor ? `after: "${cursor}",` : ""} | ||
) { | ||
edges { | ||
cursor | ||
node { | ||
@@ -236,48 +261,80 @@ id | ||
// Return a document object via lookup by name | ||
async getDocumentByName(name, version, maxRetries = 10, verifiedOnly = true) { | ||
// check if doc is in cache and entry is up to date (and correct version) | ||
if (this.isCached(name, version)) { | ||
return this.cache.get(name) | ||
async executeQuery(names, versions, userTags, userOptions = DEFAULT_FETCH_OPTIONS) { | ||
const options = { | ||
...DEFAULT_FETCH_OPTIONS, | ||
...userOptions, | ||
} | ||
// otherwise, fetch latest to cache | ||
// build query to lookup by name (and optionally version) and send request to arweave graphql server | ||
const query = this.#queryBuilder([name], version === undefined ? [] : [version], verifiedOnly) | ||
const req = await fetch('https://arweave.net/graphql', { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'Accept': 'application/json', | ||
}, | ||
body: JSON.stringify(query), | ||
}) | ||
const json = await req.json() | ||
const fetchQuery = async (cursor) => { | ||
// fetch latest to cache | ||
// build query to lookup by name (and optionally version) and send request to arweave graphql server | ||
const query = this.#queryBuilder(names, versions, userTags, options.verifiedOnly, cursor) | ||
const req = await fetch('https://arweave.net/graphql', { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'Accept': 'application/json', | ||
}, | ||
body: JSON.stringify(query), | ||
}) | ||
const json = await req.json() | ||
return json.data.transactions | ||
} | ||
const resultEdges = [] | ||
let nResults = 1 | ||
let cursor = undefined | ||
while (nResults > 0 && resultEdges.length < options.maxResults) { | ||
const newEdges = await fetchQuery(cursor) | ||
nResults = newEdges.edges.length | ||
resultEdges.push(...newEdges.edges) | ||
cursor = newEdges.cursor | ||
} | ||
// safe to get first item as we specify specific tags in the query building stage | ||
const txId = version !== undefined ? | ||
json.data.transactions.edges[0]?.node.id : | ||
json.data.transactions.edges.sort((a, b) => { | ||
const txIds = versions.length === 0 ? | ||
resultEdges.map(e => e.node.id) : | ||
resultEdges.sort((a, b) => { | ||
// we reverse sort edges if version is not defined to get latest version | ||
const getVersion = (edge) => edge.node.tags.find(tag => tag.name === VERSION).value || 0 | ||
return getVersion(b) - getVersion(a) | ||
})[0]?.node.id | ||
if (!txId) { | ||
return Promise.reject(`No transaction with name ${name} found`) | ||
} | ||
}).map(e => e.node.id) | ||
// fetch document, update cache | ||
const doc = await this.getDocumentByTxId(txId, maxRetries) | ||
this.cache.set(doc.name, doc) | ||
return doc | ||
const promises = txIds.map(txId => this.getDocumentByTxId(txId, options)) | ||
const docs = (await Promise.allSettled(promises)) | ||
.filter(p => p.status === "fulfilled") | ||
.map(p => p.value) | ||
.slice(0, userOptions.maxResults) | ||
docs.forEach(doc => this.cache.set(doc.name, doc)) | ||
return docs | ||
} | ||
// Return a document object via lookup by transaction ID. Not cached. | ||
async getDocumentByTxId(txId, maxRetries = 10, verifiedOnly = true) { | ||
// Return a list of document objects by their tags | ||
async getDocumentsByTags(tags, options = DEFAULT_FETCH_OPTIONS) { | ||
return this.executeQuery([] ,[], tags, options) | ||
} | ||
// Return a document object via lookup by name | ||
async getDocumentsByName(name, version, tags = [], options = DEFAULT_FETCH_OPTIONS) { | ||
return this.executeQuery([name], version === undefined ? [] : [version], tags, options) | ||
} | ||
// Return a document object via lookup by transaction ID | ||
async getDocumentByTxId(txId, userOptions = DEFAULT_FETCH_OPTIONS) { | ||
const options = { | ||
...DEFAULT_FETCH_OPTIONS, | ||
...userOptions | ||
} | ||
if (this.cache.has(txId)) { | ||
return this.cache.get(txId) | ||
} | ||
// ensure block with tx is confirmed (do not assume it is in cache) | ||
const txStatus = await this.pollForConfirmation(txId, maxRetries) | ||
const txStatus = await this.pollForConfirmation(txId, options.maxRetries) | ||
// fetch tx metadata | ||
const transactionMetadata = await this.client.transactions.get(txId) | ||
if (verifiedOnly && transactionMetadata.owner !== this.#key.n) { | ||
if (options.verifiedOnly && transactionMetadata.owner !== this.#key.n) { | ||
return Promise.reject(`Document is not verified. Owner address mismatched! Got: ${transactionMetadata.owner}`) | ||
@@ -284,0 +341,0 @@ } |
{ | ||
"name": "ar-wrapper", | ||
"version": "1.0.1", | ||
"version": "1.1.0", | ||
"description": "Thin wrapper around arweave-js for versioned permaweb document management", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
14498
382