Skip to content

Commit

Permalink
feat(access-client): cli and recover (#207)
Browse files Browse the repository at this point in the history
access-client cli should support #153 changes

- [x] improve d1 errors .ie space already registered
- [x] tests to validate that register space saves all the delegations
and updates isRegistered
- [x] setup cmd
- [x] whoami cmd
- [x] create space cmd
- [x] space info
- [x] delegate caps
- [x] import space from delegation
- [x] recover with client
- [x] tests migrations actually handles it properly and keeps track of
migration already applied
- [x] d1 spaces table now stores metadata and the invocation that
registered the space
- [x] we need some names on the delegations recovered so the user can
know what they are, maybe put it in the facts ?
- [x] remove duplication on the on `waitForSpaceRecover` and
`waitForVoucherRedeem`
  • Loading branch information
hugomrdias committed Dec 5, 2022
1 parent e705dae commit adb3a8d
Show file tree
Hide file tree
Showing 35 changed files with 1,587 additions and 1,086 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Migration number: 0001 2022-11-24T11:52:58.174Z
ALTER TABLE "spaces"
ADD COLUMN "metadata" JSON NOT NULL DEFAULT '"{}"';

ALTER TABLE "spaces"
ADD COLUMN "invocation" text NOT NULL;
3 changes: 3 additions & 0 deletions packages/access-api/migrations/0002_add_delegation_column.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Migration number: 0002 2022-11-29T14:41:37.991Z
ALTER TABLE "spaces"
ADD COLUMN "delegation" text DEFAULT NULL;
15 changes: 10 additions & 5 deletions packages/access-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"lint": "tsc --build && eslint '**/*.{js,ts}' && prettier --check '**/*.{js,ts,yml,json}' --ignore-path ../../.gitignore",
"dev": "scripts/cli.js dev",
"build": "scripts/cli.js build",
"check": "tsc --build",
"test": "pnpm build && tsc --build && ava --timeout 10s"
"test": "pnpm build && mocha --bail --timeout 10s -n no-warnings -n experimental-vm-modules",
"test-watch": "pnpm build && mocha --bail --timeout 10s --watch --parallel -n no-warnings -n experimental-vm-modules --watch-files src,test"
},
"author": "Hugo Dias <hugomrdias@gmail.com> (hugodias.me)",
"license": "(Apache-2.0 OR MIT)",
Expand Down Expand Up @@ -39,16 +39,18 @@
"@sentry/cli": "2.7.0",
"@types/assert": "^1.5.6",
"@types/git-rev-sync": "^2.0.0",
"@types/node": "^18.11.10",
"@types/mocha": "^10.0.1",
"@types/node": "^18.11.9",
"@types/qrcode": "^1.5.0",
"ava": "^5.1.0",
"better-sqlite3": "8.0.1",
"better-sqlite3": "8.0.0",
"buffer": "^6.0.3",
"dotenv": "^16.0.3",
"esbuild": "^0.15.16",
"git-rev-sync": "^3.0.2",
"hd-scripts": "^3.0.2",
"is-subset": "^0.1.1",
"miniflare": "^2.11.0",
"mocha": "^10.1.0",
"p-wait-for": "^5.0.0",
"process": "^0.11.10",
"readable-stream": "^4.2.0",
Expand All @@ -66,6 +68,9 @@
"jsx": true
}
},
"env": {
"mocha": true
},
"globals": {
"VERSION": "readonly",
"COMMITHASH": "readonly",
Expand Down
10 changes: 10 additions & 0 deletions packages/access-api/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,13 @@ pnpm run lint
# Run tests
pnpm run test
```

## Migrations

### Create migration

```bash
pnpm exec wrangler d1 migrations create __D1_BETA__ "<description>"
```

This will create a new file inside the `migrations` folder where you can write SQL.
48 changes: 26 additions & 22 deletions packages/access-api/scripts/migrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import split from '@databases/split-sql-query'
import sql from '@databases/sql'
import path from 'path'
import { fileURLToPath } from 'url'
import fs from 'fs'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

Expand All @@ -10,34 +11,37 @@ const sqliteFormat = {
escapeIdentifier: (_) => '',
formatValue: (_, __) => ({ placeholder: '', value: '' }),
}
const migrations = [
sql.file(`${__dirname}/../migrations/0000_create_spaces_table.sql`),
]

// const files = globbySync(`${__dirname}/../migrations/*`)
const dir = path.resolve(`${__dirname}/../migrations`)

const files = fs.readdirSync(dir)
const migrations = files.map((f) => sql.file(path.join(dir, f)))

/**
* Migrate from migration files
*
* @param {D1Database} db
*/
export async function migrate(db) {
try {
for (const m of migrations) {
/** @type {import('@databases/sql').SQLQuery[]} */
// @ts-ignore
const qs = split.default(m)
await db.batch(
qs.map((q) => {
return db.prepare(q.format(sqliteFormat).text.replace(/^--.*$/gm, ''))
})
)
}
} catch (error) {
const err = /** @type {Error} */ (error)
// eslint-disable-next-line no-console
console.error('D1 Error', {
message: err.message,
// @ts-ignore
cause: err.cause?.message,
})
const appliedMigrations = /** @type {number} */ (
await db.prepare('PRAGMA user_version').first('user_version')
)

migrations.splice(0, appliedMigrations)
const remaining = migrations.length
for (const m of migrations) {
/** @type {import('@databases/sql').SQLQuery[]} */
// @ts-ignore
const qs = split.default(m)
await db.batch(
qs.map((q) => {
return db.prepare(q.format(sqliteFormat).text.replace(/^--.*$/gm, ''))
})
)

await db
.prepare(`PRAGMA user_version = ${appliedMigrations + remaining}`)
.all()
}
}
4 changes: 4 additions & 0 deletions packages/access-api/src/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@ export interface ModuleWorker {
fetch?: ModuleWorker.FetchHandler<Env>
scheduled?: ModuleWorker.CronHandler<Env>
}

export interface D1ErrorRaw extends Error {
cause: Error & { code: string }
}
139 changes: 85 additions & 54 deletions packages/access-api/src/kvs/spaces.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,77 @@
// @ts-ignore
// eslint-disable-next-line no-unused-vars
import * as Ucanto from '@ucanto/interface'
import { delegationToString } from '@web3-storage/access/encoding'
import {
delegationToString,
stringToDelegation,
} from '@web3-storage/access/encoding'

/**
* @typedef {import('@web3-storage/access/types').SpaceD1} SpaceD1
*/

/**
* @implements {Ucanto.Failure}
*/
export class D1Error extends Error {
/** @type {true} */
get error() {
return true
}

/**
*
* @param {import('../bindings').D1ErrorRaw} error
*/
constructor(error) {
super(`${error.cause.message} (${error.cause.code})`, {
cause: error.cause,
})
this.name = 'D1Error'
this.code = error.cause.code
}
}

/**
* Spaces
*/
export class Spaces {
/**
*
* @param {KVNamespace} kv
* @param {import('workers-qb').D1QB} db
*/
constructor(kv, db) {
this.kv = kv
constructor(db) {
this.db = db
}

/**
* @param {import('@web3-storage/capabilities/types').VoucherRedeem} capability
* @param {Ucanto.Invocation<import('@web3-storage/capabilities/types').VoucherRedeem>} invocation
* @param {Ucanto.Delegation<[import('@web3-storage/access/src/types').Top]> | undefined} delegation
*/
async create(capability, invocation) {
await this.db.insert({
tableName: 'spaces',
data: {
did: capability.nb.space,
product: capability.nb.product,
email: capability.nb.identity.replace('mailto:', ''),
agent: invocation.issuer.did(),
},
})
async create(capability, invocation, delegation) {
try {
const result = await this.db.insert({
tableName: 'spaces',
data: {
did: capability.nb.space,
product: capability.nb.product,
email: capability.nb.identity.replace('mailto:', ''),
agent: invocation.issuer.did(),
metadata: JSON.stringify(invocation.facts[0]),
invocation: await delegationToString(invocation),
// eslint-disable-next-line unicorn/no-null
delegation: !delegation ? null : await delegationToString(delegation),
},
})
return { data: result }
} catch (error) {
return {
error: new D1Error(
/** @type {import('../bindings').D1ErrorRaw} */ (error)
),
}
}
}

/**
Expand Down Expand Up @@ -63,55 +100,49 @@ export class Spaces {
product: results.product,
updated_at: results.update_at,
inserted_at: results.inserted_at,
// @ts-ignore
metadata: JSON.parse(results.metadata),
})
}

/**
* Save space delegation per email
*
* @param {`mailto:${string}`} email
* @param {Ucanto.Delegation<Ucanto.Capabilities>} delegation
* @param {string} email
*/
async saveDelegation(email, delegation) {
const accs = /** @type {string[] | undefined} */ (
await this.kv.get(email, {
type: 'json',
})
)
async getByEmail(email) {
const s = await this.db.fetchAll({
tableName: 'spaces',
fields: '*',
where: {
conditions: 'email=?1',
params: [email],
},
})

if (accs) {
accs.push(await delegationToString(delegation))
await this.kv.put(email, JSON.stringify(accs))
} else {
await this.kv.put(
email,
JSON.stringify([await delegationToString(delegation)])
)
if (!s.results || s.results.length === 0) {
return
}
}

/**
* Check if we have delegations for an email
*
* @param {`mailto:${string}`} email
*/
async hasDelegations(email) {
const r = await this.kv.get(email)
return Boolean(r)
}

/**
* @param {`mailto:${string}`} email
*/
async getDelegations(email) {
const r = await this.kv.get(email, { type: 'json' })
const out = []

if (!r) {
return
for (const r of s.results) {
out.push({
did: r.did,
agent: r.agent,
email: r.email,
product: r.product,
updated_at: r.update_at,
inserted_at: r.inserted_at,
// @ts-ignore
metadata: JSON.parse(r.metadata),
delegation: !r.delegation
? undefined
: await stringToDelegation(
/** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/access/types').Top]>} */ (
r.delegation
)
),
})
}

return /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/capabilities/types').Top]>[]} */ (
r
)
return out
}
}
Loading

0 comments on commit adb3a8d

Please sign in to comment.