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(access-client): cli and recover #207

Merged
merged 12 commits into from
Dec 5, 2022
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;
Copy link
Member

Choose a reason for hiding this comment

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

Why not Blob type?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah it was just easier to do it like this, can we change this in a follow PR when we move to providers and accounts ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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 <[email protected]> (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