Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add store/get and upload/get capabilities #942

Merged
merged 15 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/capabilities/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ export const abilitiesAsStrings = [
Space.info.can,
Upload.upload.can,
Upload.add.can,
Upload.get.can,
Upload.remove.can,
Upload.list.can,
Store.store.can,
Store.add.can,
Store.get.can,
Store.remove.can,
Store.list.can,
Access.access.can,
Expand Down
22 changes: 22 additions & 0 deletions packages/capabilities/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,28 @@ export const add = capability({
},
})

/**
* Capability to get store metadata by shard CID.
* Use to check for inclusion, or get shard size and origin
*
* `nb.link` is optional to allow delegation of `store/get`
* capability for any shard cid. If link is specified, then the
* capability only allows a get for that single cid.
olizilla marked this conversation as resolved.
Show resolved Hide resolved
*
* When used as as an invocation, `nb.link` must be specified.
*/
export const get = capability({
can: 'store/get',
with: SpaceDID,
nb: Schema.struct({
/**
* shard CID to fetch info about.
*/
link: Link.optional(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should be derivable from store/add (similar to space/info)? i.e. if you were able to store/add you should be able to derive store/get capability for the same link.

https://github.com/web3-storage/ucanto/blob/43ea497ca2200f9e1e15dbbdd033ec24895edb7d/packages/interface/src/capability.ts#L122-L126

Perhaps not, write does not imply read...just thinking out loud.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is interesting point, but i'm gonna leave it separate for now.

}),
derives: equalLink,
})

/**
* Capability can be used to remove the stored CAR file from the (memory)
* space identified by `with` field.
Expand Down
36 changes: 28 additions & 8 deletions packages/capabilities/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { PieceLink } from '@web3-storage/data-segment'
import { space, info } from './space.js'
import * as provider from './provider.js'
import { top } from './top.js'
import { add, list, remove, store } from './store.js'
import * as StoreCaps from './store.js'
import * as UploadCaps from './upload.js'
import * as AccessCaps from './access.js'
import * as CustomerCaps from './customer.js'
Expand Down Expand Up @@ -221,14 +221,22 @@ export interface ChainTrackerInfoFailure extends Ucanto.Failure {
// Upload
export type Upload = InferInvokedCapability<typeof UploadCaps.upload>
export type UploadAdd = InferInvokedCapability<typeof UploadCaps.add>
export type UploadGet = InferInvokedCapability<typeof UploadCaps.get>
export type UploadRemove = InferInvokedCapability<typeof UploadCaps.remove>
export type UploadList = InferInvokedCapability<typeof UploadCaps.list>

export interface UploadNotFound extends Ucanto.Failure {
name: 'UploadNotFound'
}

export type UploadGetFailure = UploadNotFound | Ucanto.Failure

// Store
export type Store = InferInvokedCapability<typeof store>
export type StoreAdd = InferInvokedCapability<typeof add>
export type StoreRemove = InferInvokedCapability<typeof remove>
export type StoreList = InferInvokedCapability<typeof list>
export type Store = InferInvokedCapability<typeof StoreCaps.store>
export type StoreAdd = InferInvokedCapability<typeof StoreCaps.add>
export type StoreGet = InferInvokedCapability<typeof StoreCaps.get>
export type StoreRemove = InferInvokedCapability<typeof StoreCaps.remove>
export type StoreList = InferInvokedCapability<typeof StoreCaps.list>
Comment on lines +235 to +239
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update to pull each cap off of StoreCaps to be consistent with all the other caps in this file.


export type StoreAddSuccess = StoreAddSuccessDone | StoreAddSuccessUpload
export interface StoreAddSuccessDone {
Expand Down Expand Up @@ -257,6 +265,10 @@ export interface StoreItemNotFound extends Ucanto.Failure {

export type StoreRemoveFailure = StoreItemNotFound | Ucanto.Failure

export type StoreGetSuccess = StoreListItem

export type StoreGetFailure = StoreItemNotFound | Ucanto.Failure
olizilla marked this conversation as resolved.
Show resolved Hide resolved

export interface StoreListSuccess extends ListResponse<StoreListItem> {}

export interface ListResponse<R> {
Expand All @@ -271,13 +283,21 @@ export interface StoreListItem {
link: UnknownLink
size: number
origin?: UnknownLink
insertedAt: string
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add insertedAt to StoreListItem as we already want to return this info for list usage, and it means we can re-use the type for both list and get operations.

}

export interface UploadAddSuccess {
export interface UploadListItem {
root: UnknownLink
shards?: CARLink[]
insertedAt: string
updatedAt: string
Comment on lines +292 to +293
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add insertedAt and updatedAt props to UploadListItem. We want to share a timestamp with the user about when this upload was added, and these are the props we have. updatedAt currently means "when did we last add a shard to this upload", while insertedAt means "when did you first add a given root cid as an upload to this space."

}

// TODO: (olizilla) make this an UploadListItem too?
export type UploadAddSuccess = Omit<UploadListItem, 'insertedAt' | 'updatedAt'>

export type UploadGetSuccess = UploadListItem

export type UploadRemoveSuccess = UploadDidRemove | UploadDidNotRemove

export interface UploadDidRemove extends UploadAddSuccess {}
Expand All @@ -289,8 +309,6 @@ export interface UploadDidNotRemove {

export interface UploadListSuccess extends ListResponse<UploadListItem> {}

export interface UploadListItem extends UploadAddSuccess {}

// UCAN core events

export type UCANRevoke = InferInvokedCapability<typeof UCANCaps.revoke>
Expand Down Expand Up @@ -374,10 +392,12 @@ export type AbilitiesArray = [
SpaceInfo['can'],
Upload['can'],
UploadAdd['can'],
UploadGet['can'],
UploadRemove['can'],
UploadList['can'],
Store['can'],
StoreAdd['can'],
StoreGet['can'],
StoreRemove['can'],
StoreList['can'],
Access['can'],
Expand Down
32 changes: 32 additions & 0 deletions packages/capabilities/src/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,38 @@ export const add = capability({
},
})

/**
* Capability to get upload metadata by root CID.
* Use to check for inclusion, or find the shards for a root.
*
* `nb.root` is optional to allow delegation of `upload/get`
* capability for any root. If root is specified, then the
* capability only allows a get for that single cid.
*
* When used as as an invocation, `nb.root` must be specified.
*/
export const get = capability({
can: 'upload/get',
with: SpaceDID,
nb: Schema.struct({
/**
* Root CID of the DAG to fetch upload info about.
*/
root: Link.optional(),
}),
derives: (self, from) => {
const res = equalWith(self, from)
if (res.error) {
return res
}
if (!from.nb.root) {
return res
}
// root must match if specified in the proof
return equal(self.nb.root, from.nb.root, 'root')
},
})

/**
* Capability removes an upload (identified by it's root CID) from the upload
* list. Please note that removing an upload does not delete corresponding shards
Expand Down
2 changes: 1 addition & 1 deletion packages/capabilities/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function equal(child, parent, constraint) {
}

/**
* @template {Types.ParsedCapability<"store/add"|"store/remove", Types.URI<'did:'>, {link?: Types.Link<unknown, number, number, 0|1>}>} T
* @template {Types.ParsedCapability<"store/add"|"store/get"|"store/remove", Types.URI<'did:'>, {link?: Types.Link<unknown, number, number, 0|1>}>} T
* @param {T} claimed
* @param {T} delegated
* @returns {Types.Result<{}, Types.Failure>}
Expand Down
2 changes: 2 additions & 0 deletions packages/upload-api/src/store.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { storeAddProvider } from './store/add.js'
import { storeGetProvider } from './store/get.js'
import { storeListProvider } from './store/list.js'
import { storeRemoveProvider } from './store/remove.js'
import * as API from './types.js'
Expand All @@ -9,6 +10,7 @@ import * as API from './types.js'
export function createService(context) {
return {
add: storeAddProvider(context),
get: storeGetProvider(context),
list: storeListProvider(context),
remove: storeRemoveProvider(context),
}
Expand Down
29 changes: 29 additions & 0 deletions packages/upload-api/src/store/get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as Server from '@ucanto/server'
import * as Store from '@web3-storage/capabilities/store'
import * as API from '../types.js'

/**
* @param {API.StoreServiceContext} context
* @returns {API.ServiceMethod<API.StoreGet, API.StoreGetSuccess, API.Failure>}
olizilla marked this conversation as resolved.
Show resolved Hide resolved
*/
export function storeGetProvider(context) {
return Server.provide(Store.get, async ({ capability }) => {
const { link } = capability.nb
if (!link) {
return Server.fail('nb.link must be set')
}
const space = Server.DID.parse(capability.with).did()
const res = await context.storeTable.get(space, link)
if (!res) {
return {
error: {
name: 'ShardNotFound',
message: 'Shard not found'
}
}
}
return {
ok: res
}
})
}
42 changes: 21 additions & 21 deletions packages/upload-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface DebugEmail extends Email {

import {
StoreAdd,
StoreGet,
StoreAddSuccess,
StoreRemove,
StoreRemoveSuccess,
Expand All @@ -67,6 +68,7 @@ import {
StoreListSuccess,
StoreListItem,
UploadAdd,
UploadGet,
UploadAddSuccess,
UploadRemove,
UploadRemoveSuccess,
Expand Down Expand Up @@ -116,9 +118,13 @@ import {
ProviderAddFailure,
SpaceInfo,
ProviderDID,
StoreGetFailure,
UploadGetFailure,
UCANRevoke,
ListResponse,
CARLink,
StoreGetSuccess,
UploadGetSuccess,
} from '@web3-storage/capabilities/types'
import * as Capabilities from '@web3-storage/capabilities'
import { RevocationsStorage } from './types/revocations'
Expand All @@ -142,11 +148,13 @@ export type { RateLimitsStorage, RateLimit } from './types/rate-limits'
export interface Service {
store: {
add: ServiceMethod<StoreAdd, StoreAddSuccess, Failure>
get: ServiceMethod<StoreGet, StoreGetSuccess, StoreGetFailure>
remove: ServiceMethod<StoreRemove, StoreRemoveSuccess, StoreRemoveFailure>
list: ServiceMethod<StoreList, StoreListSuccess, Failure>
}
upload: {
add: ServiceMethod<UploadAdd, UploadAddSuccess, Failure>
get: ServiceMethod<UploadGet, UploadGetSuccess, UploadGetFailure>
remove: ServiceMethod<UploadRemove, UploadRemoveSuccess, Failure>
list: ServiceMethod<UploadList, UploadListSuccess, Failure>
}
Expand Down Expand Up @@ -330,9 +338,7 @@ export interface UcantoServerTestContext
fetch: typeof fetch
}

export interface StoreTestContext {
testStoreTable: TestStoreTable
}
export interface StoreTestContext {}

export interface UploadTestContext {}

Expand Down Expand Up @@ -372,37 +378,27 @@ export interface DudewhereBucket {
}

export interface StoreTable {
inspect: (link: UnknownLink) => Promise<StoreGetOk>
inspect: (link: UnknownLink) => Promise<StoreInspectOk>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the response type for inspect to be StoreInspectOk as StoreGet was confusing... and even more so now I'm adding a StoreGet.

exists: (space: DID, link: UnknownLink) => Promise<boolean>
get: (space: DID, link: UnknownLink) => Promise<StoreAddOutput | undefined>
insert: (item: StoreAddInput) => Promise<StoreAddOutput>
remove: (space: DID, link: UnknownLink) => Promise<void>
list: (
space: DID,
options?: ListOptions
) => Promise<ListResponse<StoreListItem>>
}

export interface TestStoreTable {
get(
space: DID,
link: UnknownLink
): Promise<
(StoreAddInput & StoreListItem & { insertedAt: string }) | undefined
>
) => Promise<ListResponse<StoreAddInput & StoreListItem>>
get: (space: DID,link: UnknownLink) => Promise<(StoreListItem) | undefined>
}

export interface UploadTable {
inspect: (link: UnknownLink) => Promise<UploadGetOk>
inspect: (link: UnknownLink) => Promise<UploadInspectOk>
olizilla marked this conversation as resolved.
Show resolved Hide resolved
exists: (space: DID, root: UnknownLink) => Promise<boolean>
insert: (item: UploadAddInput) => Promise<UploadAddSuccess>
remove: (space: DID, root: UnknownLink) => Promise<UploadRemoveSuccess | null>
list: (
space: DID,
options?: ListOptions
) => Promise<
ListResponse<UploadListItem & { insertedAt: string; updatedAt: string }>
>
) => Promise<ListResponse<UploadListItem>>
get: (space: DID,link: UnknownLink) => Promise<(UploadListItem) | undefined>
}

export type SpaceInfoSuccess = {
Expand Down Expand Up @@ -437,10 +433,14 @@ export interface StoreAddInput {
invocation: UCANLink
}

export interface StoreMetadata {
insertedAt: string
}

export interface StoreAddOutput
extends Omit<StoreAddInput, 'space' | 'issuer' | 'invocation'> {}

export interface StoreGetOk {
export interface StoreInspectOk {
spaces: Array<{ did: DID; insertedAt: string }>
}

Expand All @@ -452,7 +452,7 @@ export interface UploadAddInput {
invocation: UCANLink
}

export interface UploadGetOk {
export interface UploadInspectOk {
spaces: Array<{ did: DID; insertedAt: string }>
}

Expand Down
2 changes: 2 additions & 0 deletions packages/upload-api/src/upload.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { uploadAddProvider } from './upload/add.js'
import { uploadGetProvider } from './upload/get.js'
import { uploadListProvider } from './upload/list.js'
import { uploadRemoveProvider } from './upload/remove.js'
import * as API from './types.js'
Expand All @@ -9,6 +10,7 @@ import * as API from './types.js'
export function createService(context) {
return {
add: uploadAddProvider(context),
get: uploadGetProvider(context),
list: uploadListProvider(context),
remove: uploadRemoveProvider(context),
}
Expand Down
29 changes: 29 additions & 0 deletions packages/upload-api/src/upload/get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as Server from '@ucanto/server'
import * as Upload from '@web3-storage/capabilities/upload'
import * as API from '../types.js'

/**
* @param {API.UploadServiceContext} context
* @returns {API.ServiceMethod<API.UploadGet, API.UploadGetSuccess, API.Failure>}
olizilla marked this conversation as resolved.
Show resolved Hide resolved
*/
export function uploadGetProvider(context) {
return Server.provide(Upload.get, async ({ capability }) => {
const { root } = capability.nb
if (!root) {
return Server.fail('nb.root must be set')
}
const space = Server.DID.parse(capability.with).did()
const res = await context.uploadTable.get(space, root)
if (!res) {
return {
error: {
name: 'UploadNotFound',
message: 'Upload not found'
}
}
}
return {
ok: res
}
})
}
Loading
Loading