diff --git a/billing/lib/ucan-stream.js b/billing/lib/ucan-stream.js index 96646b81..0ce584e9 100644 --- a/billing/lib/ucan-stream.js +++ b/billing/lib/ucan-stream.js @@ -16,13 +16,14 @@ export const findSpaceUsageDeltas = messages => { /** @type {number|undefined} */ let size if (isReceiptForCapability(message, StoreCaps.add) && isStoreAddSuccess(message.out)) { - size = message.value.att[0].nb?.size + size = message.out.ok.allocated } else if (isReceiptForCapability(message, StoreCaps.remove) && isStoreRemoveSuccess(message.out)) { size = -message.out.ok.size } - // message is not a valid store/add or store/remove receipt - if (size == null) { + // Is message is a repeat store/add for the same shard or not a valid + // store/add or store/remove receipt? + if (size == 0 || size == null) { continue } diff --git a/billing/package.json b/billing/package.json index 4bcf738f..f3f4b0fa 100644 --- a/billing/package.json +++ b/billing/package.json @@ -13,7 +13,7 @@ "@sentry/serverless": "^7.74.1", "@ucanto/interface": "^9.0.0", "@ucanto/server": "^9.0.1", - "@web3-storage/capabilities": "^11.3.1", + "@web3-storage/capabilities": "^13.0.0", "big.js": "^6.2.1", "multiformats": "^12.1.2", "p-retry": "^6.1.0", diff --git a/billing/test/lib/ucan-stream.js b/billing/test/lib/ucan-stream.js index d55d5cd4..2bd27dae 100644 --- a/billing/test/lib/ucan-stream.js +++ b/billing/test/lib/ucan-stream.js @@ -41,7 +41,7 @@ export const test = { aud: await randomDID(), cid: randomLink() }, - out: { ok: { status: 'upload' } }, + out: { ok: { status: 'upload', allocated: 138 } }, ts: new Date() }, { type: 'receipt', @@ -95,7 +95,7 @@ export const test = { aud: consumer.provider, cid: randomLink() }, - out: { ok: { status: 'upload' } }, + out: { ok: { status: 'upload', allocated: 138 } }, ts: new Date(from.getTime() + 1) }, { type: 'receipt', @@ -113,7 +113,7 @@ export const test = { aud: consumer.provider, cid: randomLink() }, - out: { ok: { status: 'upload' } }, + out: { ok: { status: 'upload', allocated: 1138 } }, ts: new Date(from.getTime() + 2) }] @@ -142,5 +142,49 @@ export const test = { d.delta === r.value.att[0].nb?.size ))) } + }, + 'should filter non-allocating store/add messages': async (/** @type {import('entail').assert} */ assert, ctx) => { + const consumer = await randomConsumer() + + await ctx.consumerStore.put(consumer) + + const from = new Date() + + /** @type {import('../../lib/api.js').UcanReceiptMessage<[import('@web3-storage/capabilities/types').StoreAdd]>[]} */ + const receipts = [{ + type: 'receipt', + carCid: randomLink(), + invocationCid: randomLink(), + value: { + att: [{ + with: Schema.did({ method: 'key' }).from(consumer.consumer), + can: 'store/add', + nb: { + link: randomLink(), + size: 138 + } + }], + aud: consumer.provider, + cid: randomLink() + }, + // allocated: 0 indicates this shard was previously stored in this space + out: { ok: { status: 'upload', allocated: 0 } }, + ts: new Date(from.getTime() + 1) + }] + + const deltas = findSpaceUsageDeltas(receipts) + + for (const d of deltas) { + const res = await storeSpaceUsageDelta(d, ctx) + assert.ok(res.ok) + } + + const res = await ctx.spaceDiffStore.list({ + provider: consumer.provider, + space: consumer.consumer, + from + }, { size: 1000 }) + assert.ok(res.ok) + assert.equal(res.ok.results.length, 0) } } diff --git a/package-lock.json b/package-lock.json index bf5045de..137b5fa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,7 @@ "@sentry/serverless": "^7.74.1", "@ucanto/interface": "^9.0.0", "@ucanto/server": "^9.0.1", - "@web3-storage/capabilities": "^11.3.1", + "@web3-storage/capabilities": "^13.0.0", "big.js": "^6.2.1", "multiformats": "^12.1.2", "p-retry": "^6.1.0", @@ -84,6 +84,38 @@ "version": "0.12.2", "license": "MIT" }, + "billing/node_modules/@web3-storage/capabilities": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@web3-storage/capabilities/-/capabilities-13.0.0.tgz", + "integrity": "sha512-SSviDXFweCu8FhaQ7BjsK1WDPBdwoduJhfD2DFRKkZT/V25vdBkqtW4qVc6tfO5IhTVxRpnn62PmbIbbcHzBRQ==", + "dependencies": { + "@ucanto/core": "^9.0.1", + "@ucanto/interface": "^9.0.0", + "@ucanto/principal": "^9.0.0", + "@ucanto/transport": "^9.0.0", + "@ucanto/validator": "^9.0.1", + "@web3-storage/data-segment": "^3.2.0" + } + }, + "billing/node_modules/@web3-storage/data-segment": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@web3-storage/data-segment/-/data-segment-3.2.0.tgz", + "integrity": "sha512-SM6eNumXzrXiQE2/J59+eEgCRZNYPxKhRoHX2QvV3/scD4qgcf4g+paWBc3UriLEY1rCboygGoPsnqYJNyZyfA==", + "dependencies": { + "@ipld/dag-cbor": "^9.0.5", + "multiformats": "^11.0.2", + "sync-multihash-sha2": "^1.0.0" + } + }, + "billing/node_modules/@web3-storage/data-segment/node_modules/multiformats": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-11.0.2.tgz", + "integrity": "sha512-b5mYMkOkARIuVZCpvijFj9a6m5wMVLC7cf/jIPd5D/ARDOfLC5+IFkbgDXQgcU2goIsTD/O9NY4DI/Mt4OGvlg==", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, "billing/node_modules/docker-compose": { "version": "0.24.2", "dev": true, @@ -6344,9 +6376,9 @@ } }, "node_modules/@web3-storage/upload-api": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@web3-storage/upload-api/-/upload-api-7.3.5.tgz", - "integrity": "sha512-pgRPRGdTwRRytNb6H3up82gogcjrnxmP9ZbRmBuDs6QYU1q+MWDOYyNjIEgtomsjYFLMacKIA+VpPi7HZkMCJQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@web3-storage/upload-api/-/upload-api-8.0.0.tgz", + "integrity": "sha512-R5K0nMBkqJDKiodrJ1kmHQrlhaDiV2EFPaMRteWx3XAMl5E870EhL0Gq9H5/fGzmffMo0e1J8iNl2Wz/NH+2SA==", "dependencies": { "@ucanto/client": "^9.0.0", "@ucanto/interface": "^9.0.0", @@ -6354,8 +6386,8 @@ "@ucanto/server": "^9.0.1", "@ucanto/transport": "^9.0.0", "@ucanto/validator": "^9.0.1", - "@web3-storage/access": "^18.0.4", - "@web3-storage/capabilities": "^12.1.0", + "@web3-storage/access": "^18.0.5", + "@web3-storage/capabilities": "^13.0.0", "@web3-storage/did-mailto": "^2.1.0", "@web3-storage/filecoin-api": "^4.3.0", "multiformats": "^12.1.2", @@ -6366,9 +6398,9 @@ } }, "node_modules/@web3-storage/upload-api/node_modules/@web3-storage/access": { - "version": "18.0.4", - "resolved": "https://registry.npmjs.org/@web3-storage/access/-/access-18.0.4.tgz", - "integrity": "sha512-VS47jDGUtf23CIX3ldxr9knCcV3FO9TNUDSWjssmWQ7keD0RU+qoQZd+9ce9QUmUMYM3RYmtf8qocY4aCFXKgA==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@web3-storage/access/-/access-18.0.5.tgz", + "integrity": "sha512-dTojMu7UWb7esbnX3F18eTC+hwlYkDhxBZAogmHX7legRGRK5MwwoRMpk8qz6zI6ImUmkApWFYrF2U8OaGC0bQ==", "dependencies": { "@ipld/car": "^5.1.1", "@ipld/dag-ucan": "^3.4.0", @@ -6390,7 +6422,7 @@ "uint8arrays": "^4.0.6" } }, - "node_modules/@web3-storage/upload-api/node_modules/@web3-storage/capabilities": { + "node_modules/@web3-storage/upload-api/node_modules/@web3-storage/access/node_modules/@web3-storage/capabilities": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/@web3-storage/capabilities/-/capabilities-12.1.0.tgz", "integrity": "sha512-SlYdPqCokDHb55zlZOvh+n8uEMOrEU413Z1MzQ8HvULpbzfcEtGyOiDgrAhdNEZtPnWHqaUEtU7o829Yw2Ra5w==", @@ -6403,6 +6435,19 @@ "@web3-storage/data-segment": "^3.2.0" } }, + "node_modules/@web3-storage/upload-api/node_modules/@web3-storage/capabilities": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@web3-storage/capabilities/-/capabilities-13.0.0.tgz", + "integrity": "sha512-SSviDXFweCu8FhaQ7BjsK1WDPBdwoduJhfD2DFRKkZT/V25vdBkqtW4qVc6tfO5IhTVxRpnn62PmbIbbcHzBRQ==", + "dependencies": { + "@ucanto/core": "^9.0.1", + "@ucanto/interface": "^9.0.0", + "@ucanto/principal": "^9.0.0", + "@ucanto/transport": "^9.0.0", + "@ucanto/validator": "^9.0.1", + "@web3-storage/data-segment": "^3.2.0" + } + }, "node_modules/@web3-storage/upload-api/node_modules/@web3-storage/data-segment": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@web3-storage/data-segment/-/data-segment-3.2.0.tgz", @@ -16409,8 +16454,8 @@ "@ucanto/validator": "^9.0.1", "@web-std/fetch": "^4.1.0", "@web3-storage/access": "^18.0.5", - "@web3-storage/capabilities": "^12.1.0", - "@web3-storage/upload-api": "^7.3.5", + "@web3-storage/capabilities": "^13.0.0", + "@web3-storage/upload-api": "^8.0.0", "@web3-storage/w3infra-ucan-invocation": "*", "multiformats": "^12.1.2", "nanoid": "^5.0.2", @@ -16461,7 +16506,7 @@ "uint8arrays": "^4.0.6" } }, - "upload-api/node_modules/@web3-storage/capabilities": { + "upload-api/node_modules/@web3-storage/access/node_modules/@web3-storage/capabilities": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/@web3-storage/capabilities/-/capabilities-12.1.0.tgz", "integrity": "sha512-SlYdPqCokDHb55zlZOvh+n8uEMOrEU413Z1MzQ8HvULpbzfcEtGyOiDgrAhdNEZtPnWHqaUEtU7o829Yw2Ra5w==", @@ -16474,6 +16519,19 @@ "@web3-storage/data-segment": "^3.2.0" } }, + "upload-api/node_modules/@web3-storage/capabilities": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@web3-storage/capabilities/-/capabilities-13.0.0.tgz", + "integrity": "sha512-SSviDXFweCu8FhaQ7BjsK1WDPBdwoduJhfD2DFRKkZT/V25vdBkqtW4qVc6tfO5IhTVxRpnn62PmbIbbcHzBRQ==", + "dependencies": { + "@ucanto/core": "^9.0.1", + "@ucanto/interface": "^9.0.0", + "@ucanto/principal": "^9.0.0", + "@ucanto/transport": "^9.0.0", + "@ucanto/validator": "^9.0.1", + "@web3-storage/data-segment": "^3.2.0" + } + }, "upload-api/node_modules/@web3-storage/data-segment": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@web3-storage/data-segment/-/data-segment-3.2.0.tgz", diff --git a/upload-api/package.json b/upload-api/package.json index f3ad7f4c..82d50e0d 100644 --- a/upload-api/package.json +++ b/upload-api/package.json @@ -24,8 +24,8 @@ "@ucanto/validator": "^9.0.1", "@web-std/fetch": "^4.1.0", "@web3-storage/access": "^18.0.5", - "@web3-storage/capabilities": "^12.1.0", - "@web3-storage/upload-api": "^7.3.5", + "@web3-storage/capabilities": "^13.0.0", + "@web3-storage/upload-api": "^8.0.0", "@web3-storage/w3infra-ucan-invocation": "*", "multiformats": "^12.1.2", "nanoid": "^5.0.2", diff --git a/upload-api/tables/lib.js b/upload-api/tables/lib.js new file mode 100644 index 00000000..6cf35c73 --- /dev/null +++ b/upload-api/tables/lib.js @@ -0,0 +1,23 @@ +import { Failure } from '@ucanto/server' + +export class RecordNotFound extends Failure { + constructor () { + super() + this.name = /** @type {const} */ ('RecordNotFound') + } + + describe () { + return 'record not found' + } +} + +export class RecordKeyConflict extends Failure { + constructor () { + super() + this.name = /** @type {const} */ ('RecordKeyConflict') + } + + describe () { + return 'record key conflict' + } +} diff --git a/upload-api/tables/store.js b/upload-api/tables/store.js index 58b9a5d2..1d804438 100644 --- a/upload-api/tables/store.js +++ b/upload-api/tables/store.js @@ -8,10 +8,14 @@ import { import { marshall, unmarshall } from '@aws-sdk/util-dynamodb' import { CID } from 'multiformats/cid' import * as Link from 'multiformats/link' +import { RecordKeyConflict, RecordNotFound } from './lib.js' -/** @typedef {import('@web3-storage/upload-api').StoreAddInput} StoreAddInput */ -/** @typedef {import('@web3-storage/upload-api').StoreAddOutput} StoreAddOutput */ -/** @typedef {import('@web3-storage/upload-api').StoreListItem} StoreListItem */ +/** + * @typedef {import('@web3-storage/upload-api').StoreTable} StoreTable + * @typedef {import('@web3-storage/upload-api').StoreAddInput} StoreAddInput + * @typedef {import('@web3-storage/upload-api').StoreAddOutput} StoreAddOutput + * @typedef {import('@web3-storage/upload-api').StoreListItem} StoreListItem + */ /** * Abstraction layer to handle operations on Store Table. @@ -20,6 +24,7 @@ import * as Link from 'multiformats/link' * @param {string} tableName * @param {object} [options] * @param {string} [options.endpoint] + * @returns {StoreTable} */ export function createStoreTable(region, tableName, options = {}) { const dynamoDb = new DynamoDBClient({ @@ -33,7 +38,7 @@ export function createStoreTable(region, tableName, options = {}) { /** * @param {DynamoDBClient} dynamoDb * @param {string} tableName - * @returns {import('@web3-storage/upload-api').StoreTable} + * @returns {StoreTable} */ export function useStoreTable(dynamoDb, tableName) { return { @@ -42,6 +47,7 @@ export function useStoreTable(dynamoDb, tableName) { * * @param {import('@ucanto/interface').DID} space * @param {import('@web3-storage/upload-api').UnknownLink} link + * @returns {ReturnType} */ exists: async (space, link) => { const cmd = new GetItemCommand({ @@ -55,16 +61,16 @@ export function useStoreTable(dynamoDb, tableName) { try { const response = await dynamoDb.send(cmd) - return response?.Item !== undefined + return { ok: Boolean(response.Item) } } catch { - return false + return { ok: false } } }, /** * Bind a link CID to an account * * @param {StoreAddInput} item - * @returns {Promise} + * @returns {ReturnType} */ insert: async ({ space, link, origin, size, issuer, invocation }) => { const insertedAt = new Date().toISOString() @@ -82,16 +88,26 @@ export function useStoreTable(dynamoDb, tableName) { const cmd = new PutItemCommand({ TableName: tableName, Item: marshall(item, { removeUndefinedValues: true }), + ConditionExpression: 'attribute_not_exists(#S) AND attribute_not_exists(#L)', + ExpressionAttributeNames: { '#S': 'space', '#L': 'link' } }) - await dynamoDb.send(cmd) - return { link, size, ...(origin && { origin }) } + try { + await dynamoDb.send(cmd) + } catch (/** @type {any} */ err) { + if (err.name === 'ConditionalCheckFailedException') { + return { error: new RecordKeyConflict() } + } + throw err + } + return { ok: { link, size, ...(origin && { origin }) } } }, /** * Unbinds a link CID to an account * * @param {import('@ucanto/interface').DID} space * @param {import('@web3-storage/upload-api').UnknownLink} link + * @returns {ReturnType} */ remove: async (space, link) => { const cmd = new DeleteItemCommand({ @@ -100,18 +116,32 @@ export function useStoreTable(dynamoDb, tableName) { space, link: link.toString(), }), + ConditionExpression: 'attribute_exists(#S) AND attribute_exists(#L)', + ExpressionAttributeNames: { '#S': 'space', '#L': 'link' }, + ReturnValues: 'ALL_OLD' }) - await dynamoDb.send(cmd) + try { + const res = await dynamoDb.send(cmd) + if (!res.Attributes) { + throw new Error('missing return values') + } + + const raw = unmarshall(res.Attributes) + return { ok: { size: Number(raw.size) } } + } catch (/** @type {any} */ err) { + if (err.name === 'ConditionalCheckFailedException') { + return { error: new RecordNotFound() } + } + throw err + } }, /** * List all CARs bound to an account * - * @typedef {import('@web3-storage/upload-api').ListResponse} ListResponse - * * @param {import('@ucanto/interface').DID} space * @param {import('@web3-storage/upload-api').ListOptions} [options] - * @returns {Promise} + * @returns {ReturnType} */ list: async (space, options = {}) => { const exclusiveStartKey = options.cursor @@ -148,16 +178,19 @@ export function useStoreTable(dynamoDb, tableName) { const before = options.pre ? lastLinkCID : firstLinkCID const after = options.pre ? firstLinkCID : lastLinkCID return { - size: results.length, - before, - after, - cursor: after, - results: options.pre ? results.reverse() : results, + ok: { + size: results.length, + before, + after, + cursor: after, + results: options.pre ? results.reverse() : results, + } } }, /** * @param {import('@web3-storage/upload-api').DID} space * @param {import('@web3-storage/upload-api').UnknownLink} link + * @returns {ReturnType} */ async get(space, link) { const item = { @@ -171,18 +204,17 @@ export function useStoreTable(dynamoDb, tableName) { } const response = await dynamoDb.send(new GetItemCommand(params)) - if (response?.Item) { - const { space, link, size, origin, issuer, invocation, insertedAt } = - unmarshall(response?.Item) + if (!response.Item) { + return { error: new RecordNotFound() } + } - return { - space, - link: Link.parse(link), - size: Number(size), - ...(origin ? { origin: Link.parse(origin) } : {}), - issuer, - invocation: Link.parse(invocation), - insertedAt, + const raw = unmarshall(response.Item) + return { + ok: { + link: Link.parse(raw.link), + size: Number(raw.size), + ...(raw.origin ? { origin: Link.parse(origin) } : {}), + insertedAt: raw.insertedAt, } } }, @@ -190,7 +222,8 @@ export function useStoreTable(dynamoDb, tableName) { /** * Get information about a CID. * - * @param {import('@web3-storage/upload-api').UnknownLink} link + * @param {import('@web3-storage/upload-api').UnknownLink} link + * @returns {ReturnType} */ inspect: async (link) => { const response = await dynamoDb.send(new QueryCommand({ @@ -202,15 +235,17 @@ export function useStoreTable(dynamoDb, tableName) { } })) return { - spaces: response.Items ? response.Items.map( - i => { - const item = unmarshall(i) - return ({ - did: item.space, - insertedAt: item.insertedAt - }) - } - ) : [] + ok: { + spaces: response.Items ? response.Items.map( + i => { + const item = unmarshall(i) + return ({ + did: item.space, + insertedAt: item.insertedAt + }) + } + ) : [] + } } } } diff --git a/upload-api/tables/upload.js b/upload-api/tables/upload.js index 200b472e..a9fd2688 100644 --- a/upload-api/tables/upload.js +++ b/upload-api/tables/upload.js @@ -7,9 +7,13 @@ import { } from '@aws-sdk/client-dynamodb' import { marshall, unmarshall } from '@aws-sdk/util-dynamodb' import { CID } from 'multiformats/cid' +import { RecordNotFound } from './lib.js' -/** @typedef {import('@web3-storage/upload-api').UploadAddSuccess} UploadAddResult */ -/** @typedef {import('@web3-storage/upload-api').UploadListItem} UploadListItem */ +/** + * @typedef {import('@web3-storage/upload-api').UploadTable} UploadTable + * @typedef {import('@web3-storage/upload-api').UploadAddSuccess} UploadAddResult + * @typedef {import('@web3-storage/upload-api').UploadListItem} UploadListItem + */ /** * Abstraction layer to handle operations on Upload Table. @@ -18,7 +22,7 @@ import { CID } from 'multiformats/cid' * @param {string} tableName * @param {object} [options] * @param {string} [options.endpoint] - * @returns {import('@web3-storage/upload-api').UploadTable} + * @returns {UploadTable} */ export function createUploadTable(region, tableName, options = {}) { const dynamoDb = new DynamoDBClient({ @@ -31,7 +35,7 @@ export function createUploadTable(region, tableName, options = {}) { /** * @param {DynamoDBClient} dynamoDb * @param {string} tableName - * @returns {import('@web3-storage/upload-api').UploadTable} + * @returns {UploadTable} */ export function useUploadTable(dynamoDb, tableName) { return { @@ -40,6 +44,7 @@ export function useUploadTable(dynamoDb, tableName) { * * @param {import('@ucanto/interface').DID} space * @param {import('@web3-storage/upload-api').UnknownLink} root + * @returns {ReturnType} */ get: async (space, root) => { const cmd = new GetItemCommand({ @@ -51,13 +56,17 @@ export function useUploadTable(dynamoDb, tableName) { AttributesToGet: ['space', 'root', 'shards', 'insertedAt', 'updatedAt'], }) const res = await dynamoDb.send(cmd) - return res.Item ? toUploadListItem(unmarshall(res.Item)) : undefined + if (!res.Item) { + return { error: new RecordNotFound() } + } + return { ok: toUploadListItem(unmarshall(res.Item)) } }, /** * Check if the given data CID is bound to a space DID * * @param {import('@ucanto/interface').DID} space * @param {import('@web3-storage/upload-api').UnknownLink} root + * @returns {ReturnType} */ exists: async (space, root) => { const cmd = new GetItemCommand({ @@ -71,9 +80,9 @@ export function useUploadTable(dynamoDb, tableName) { try { const response = await dynamoDb.send(cmd) - return response?.Item !== undefined + return { ok: Boolean(response.Item) } } catch { - return false + return { ok: false } } }, /** @@ -82,9 +91,9 @@ export function useUploadTable(dynamoDb, tableName) { * @typedef {import('@web3-storage/upload-api').UploadAddInput} UploadAddInput * * @param {UploadAddInput} item - * @returns {Promise} + * @returns {ReturnType} */ - insert: async ({ space, root, shards = [], issuer, invocation }) => { + upsert: async ({ space, root, shards = [], issuer, invocation }) => { const insertedAt = new Date().toISOString() const shardSet = new Set(shards.map((s) => s.toString())) @@ -125,13 +134,14 @@ export function useUploadTable(dynamoDb, tableName) { throw new Error('Missing `Attributes` property on DynamoDB response') } - return toUploadAddResult(unmarshall(res.Attributes)) + return { ok: toUploadAddResult(unmarshall(res.Attributes)) } }, /** * Remove an upload from an account * * @param {import('@ucanto/interface').DID} space * @param {import('@web3-storage/upload-api').UnknownLink} root + * @returns {ReturnType} */ remove: async (space, root) => { const cmd = new DeleteItemCommand({ @@ -140,21 +150,31 @@ export function useUploadTable(dynamoDb, tableName) { space, root: root.toString(), }), + ConditionExpression: 'attribute_exists(#S) AND attribute_exists(#R)', + ExpressionAttributeNames: { '#S': 'space', '#R': 'root' }, ReturnValues: 'ALL_OLD', }) - // return the removed object so caller may remove all shards - const res = await dynamoDb.send(cmd) - if (res.Attributes === undefined) { - return null + try { + // return the removed object so caller may remove all shards + const res = await dynamoDb.send(cmd) + if (res.Attributes === undefined) { + throw new Error('missing return values') + } + const raw = unmarshall(res.Attributes) + return { ok: toUploadAddResult(raw) } + } catch (/** @type {any} */ err) { + if (err.name === 'ConditionalCheckFailedException') { + return { error: new RecordNotFound() } + } + throw err } - const raw = unmarshall(res.Attributes) - return toUploadAddResult(raw) }, /** * List all CARs bound to an account * * @param {string} space * @param {import('@web3-storage/upload-api').ListOptions} [options] + * @returns {ReturnType} */ list: async (space, options = {}) => { const exclusiveStartKey = options.cursor @@ -191,18 +211,21 @@ export function useUploadTable(dynamoDb, tableName) { const before = options.pre ? lastRootCID : firstRootCID const after = options.pre ? firstRootCID : lastRootCID return { - size: results.length, - before, - after, - cursor: after, - results: options.pre ? results.reverse() : results, + ok: { + size: results.length, + before, + after, + cursor: after, + results: options.pre ? results.reverse() : results, + } } }, /** * Get information about a CID. * - * @param {import('@web3-storage/upload-api').UnknownLink} link + * @param {import('@web3-storage/upload-api').UnknownLink} link + * @returns {ReturnType} */ inspect: async (link) => { const response = await dynamoDb.send(new QueryCommand({ @@ -214,15 +237,15 @@ export function useUploadTable(dynamoDb, tableName) { } })) return { - spaces: response.Items ? response.Items.map( - i => { + ok: { + spaces: (response.Items ?? []).map(i => { const item = unmarshall(i) return ({ did: item.space, insertedAt: item.insertedAt }) - } - ) : [] + }) + } } } }