Fork of github.com/did-method-plc/did-method-plc

export routes

dholms d66ceb43 bdccd99e

Changed files
+114 -77
packages
lib
src
server
+11 -4
packages/lib/src/client.ts
···
}
async getDocumentData(did: string): Promise<t.DocumentData> {
-
const res = await axios.get(`${this.url}/data/${encodeURIComponent(did)}`)
+
const res = await axios.get(`${this.url}/${encodeURIComponent(did)}/data`)
return res.data
}
-
async getOperationLog(did: string): Promise<t.Operation[]> {
-
const res = await axios.get(`${this.url}/log/${encodeURIComponent(did)}`)
+
async getOperationLog(
+
did: string,
+
includeNull = false,
+
): Promise<t.Operation[]> {
+
let url = `${this.url}/${encodeURIComponent(did)}/log`
+
if (includeNull) {
+
url += '?includeNull=true'
+
}
+
const res = await axios.get(url)
return res.data.log
}
···
}
async getLastOp(did: string): Promise<t.Operation> {
-
const res = await axios.get(`${this.url}/last/${encodeURIComponent(did)}`)
+
const res = await axios.get(`${this.url}/${encodeURIComponent(did)}/last`)
return res.data
}
+41 -46
packages/server/src/db/index.ts
···
-
import { Generated, Kysely, Migrator, PostgresDialect, sql } from 'kysely'
+
import { Kysely, Migrator, PostgresDialect, Selectable, sql } from 'kysely'
import { Pool as PgPool, types as pgTypes } from 'pg'
import { CID } from 'multiformats/cid'
-
import { cidForCbor, check } from '@atproto/common'
+
import { cidForCbor } from '@atproto/common'
import * as plc from '@did-plc/lib'
import { ServerError } from '../error'
import * as migrations from '../migrations'
-
import { OpLogExport, PlcDatabase } from './types'
+
import { DatabaseSchema, OperationsTable, PlcDatabase } from './types'
import MockDatabase from './mock'
export * from './mock'
···
}
async validateAndAddOp(did: string, proposed: plc.Operation): Promise<void> {
-
const ops = await this._opsForDid(did)
+
const ops = await this.indexedOpsForDid(did)
// throws if invalid
const { nullified, prev } = await plc.assureValidNextOp(did, ops, proposed)
const cid = await cidForCbor(proposed)
···
return found ? CID.parse(found.cid) : null
}
-
async opsForDid(did: string): Promise<plc.OpOrTombstone[]> {
-
const ops = await this._opsForDid(did)
-
return ops.map((op) => {
-
if (check.is(op.operation, plc.def.createOpV1)) {
-
return plc.normalizeOp(op.operation)
-
}
-
return op.operation
-
})
+
async opsForDid(did: string): Promise<plc.CompatibleOpOrTombstone[]> {
+
const ops = await this.indexedOpsForDid(did)
+
return ops.map((op) => op.operation)
}
-
async _opsForDid(did: string): Promise<plc.IndexedOperation[]> {
-
const res = await this.db
+
async indexedOpsForDid(
+
did: string,
+
includeNullified = false,
+
): Promise<plc.IndexedOperation[]> {
+
let builder = this.db
.selectFrom('operations')
.selectAll()
.where('did', '=', did)
-
.where('nullified', '=', false)
.orderBy('createdAt', 'asc')
-
.execute()
-
+
if (!includeNullified) {
+
builder = builder.where('nullified', '=', false)
+
}
+
const res = await builder.execute()
return res.map((row) => ({
did: row.did,
operation: row.operation,
···
}))
}
-
async fullExport(): Promise<Record<string, OpLogExport>> {
-
return {}
-
// const res = await this.db
-
// .selectFrom('operations')
-
// .selectAll()
-
// .orderBy('did')
-
// .orderBy('createdAt')
-
// .execute()
-
// return res.reduce((acc, cur) => {
-
// acc[cur.did] ??= []
-
// acc[cur.did].push({
-
// op: cur.operation),
-
// nullified: cur.nullified === 1,
-
// createdAt: cur.createdAt,
-
// })
-
// return acc
-
// }, {} as Record<string, OpLogExport>)
+
async lastOpForDid(did: string): Promise<plc.CompatibleOpOrTombstone | null> {
+
const res = await this.db
+
.selectFrom('operations')
+
.selectAll()
+
.where('did', '=', did)
+
.where('nullified', '=', false)
+
.orderBy('createdAt', 'desc')
+
.limit(1)
+
.executeTakeFirst()
+
return res?.operation ?? null
+
}
+
+
async exportOps(
+
count: number,
+
after?: Date,
+
): Promise<Selectable<OperationsTable>[]> {
+
let builder = this.db
+
.selectFrom('operations')
+
.selectAll()
+
.orderBy('createdAt', 'desc')
+
.limit(count)
+
if (after) {
+
builder = builder.where('createdAt', '>', after)
+
}
+
return builder.execute()
}
}
export default Database
-
-
interface OperationsTable {
-
did: string
-
operation: plc.CompatibleOpOrTombstone
-
cid: string
-
nullified: boolean
-
createdAt: Generated<Date>
-
}
-
-
interface DatabaseSchema {
-
operations: OperationsTable
-
}
+26 -7
packages/server/src/db/mock.ts
···
import { cidForCbor, check } from '@atproto/common'
import * as plc from '@did-plc/lib'
+
import { Selectable } from 'kysely'
import { ServerError } from '../error'
-
import { OpLogExport, PlcDatabase } from './types'
+
import { OperationsTable, PlcDatabase } from './types'
type Contents = Record<string, plc.IndexedOperation[]>
···
}
}
-
async opsForDid(did: string): Promise<plc.OpOrTombstone[]> {
-
const ops = await this._opsForDid(did)
+
async opsForDid(did: string): Promise<plc.CompatibleOpOrTombstone[]> {
+
const ops = await this.indexedOpsForDid(did)
return ops.map((op) => {
if (check.is(op.operation, plc.def.createOpV1)) {
return plc.normalizeOp(op.operation)
···
})
}
-
async _opsForDid(did: string): Promise<plc.IndexedOperation[]> {
-
return this.contents[did] ?? []
+
async indexedOpsForDid(
+
did: string,
+
includeNull = false,
+
): Promise<plc.IndexedOperation[]> {
+
const ops = this.contents[did] ?? []
+
if (includeNull) {
+
return ops
+
}
+
return ops.filter((op) => op.nullified === false)
}
-
async fullExport(): Promise<Record<string, OpLogExport>> {
-
return {}
+
async lastOpForDid(did: string): Promise<plc.CompatibleOpOrTombstone | null> {
+
const op = this.contents[did]?.at(-1)
+
+
if (!op) return null
+
return op.operation
+
}
+
+
// disabled in mocks
+
async exportOps(
+
_count: number,
+
_after?: Date,
+
): Promise<Selectable<OperationsTable>[]> {
+
return []
}
}
+17 -8
packages/server/src/db/types.ts
···
import * as plc from '@did-plc/lib'
+
import { Generated, Selectable } from 'kysely'
export interface PlcDatabase {
close(): Promise<void>
healthCheck(): Promise<void>
validateAndAddOp(did: string, proposed: plc.Operation): Promise<void>
-
opsForDid(did: string): Promise<plc.OpOrTombstone[]>
-
_opsForDid(did: string): Promise<plc.IndexedOperation[]>
-
fullExport(): Promise<Record<string, OpLogExport>>
+
opsForDid(did: string): Promise<plc.CompatibleOpOrTombstone[]>
+
indexedOpsForDid(
+
did: string,
+
includeNull?: boolean,
+
): Promise<plc.IndexedOperation[]>
+
lastOpForDid(did: string): Promise<plc.CompatibleOpOrTombstone | null>
+
exportOps(count: number, after?: Date): Promise<Selectable<OperationsTable>[]>
}
-
export type OpLogExport = OpExport[]
-
-
export type OpExport = {
-
op: Record<string, unknown>
+
export interface OperationsTable {
+
did: string
+
operation: plc.CompatibleOpOrTombstone
+
cid: string
nullified: boolean
-
createdAt: string
+
createdAt: Generated<Date>
+
}
+
+
export interface DatabaseSchema {
+
operations: OperationsTable
}
+19 -12
packages/server/src/routes.ts
···
res.send({ version })
})
-
// @TODO paginate & test this
+
// Export ops in the form of paginated json lines
router.get('/export', async function (req, res) {
-
const fullExport = await ctx.db.fullExport()
+
const parsedCount = parseInt(req.query.count)
+
const count = isNaN(parsedCount) ? 1000 : Math.min(parsedCount, 1000)
+
const after = req.query.after ? new Date(req.query.after) : undefined
+
const ops = await ctx.db.exportOps(count, after)
res.setHeader('content-type', 'application/jsonlines')
res.status(200)
-
for (const [did, ops] of Object.entries(fullExport)) {
-
const line = JSON.stringify({ did, ops })
+
for (const op of ops) {
+
const line = JSON.stringify(op)
res.write(line)
res.write('\n')
}
···
})
// Get data for a DID document
-
router.get('/data/:did', async function (req, res) {
+
router.get('/:did/data', async function (req, res) {
const { did } = req.params
const log = await ctx.db.opsForDid(did)
if (log.length === 0) {
···
})
// Get operation log for a DID
-
router.get('/log/:did', async function (req, res) {
+
router.get('/:did/log', async function (req, res) {
const { did } = req.params
-
const log = await ctx.db.opsForDid(did)
+
const includeNull = req.query.includeNull === 'true'
+
+
const log = includeNull
+
? await ctx.db.indexedOpsForDid(did, includeNull)
+
: await ctx.db.opsForDid(did)
+
if (log.length === 0) {
throw new ServerError(404, `DID not registered: ${did}`)
}
···
})
// Get the most recent operation in the log for a DID
-
router.get('/last/:did', async function (req, res) {
+
router.get('/:did/last', async function (req, res) {
const { did } = req.params
-
const log = await ctx.db.opsForDid(did)
-
const curr = log.at(-1)
-
if (!curr) {
+
const last = await ctx.db.lastOpForDid(did)
+
if (!last) {
throw new ServerError(404, `DID not registered: ${did}`)
}
-
res.json(curr)
+
res.json(last)
})
// Update or create a DID doc