crud tests
This commit is contained in:
parent
3ef39fe34a
commit
dad56e802f
packages
@ -27,15 +27,17 @@ export const describeRepoResponse = z.object({
|
||||
export type DescribeRepoResponse = z.infer<typeof describeRepoResponse>
|
||||
|
||||
export const listRecordsParams = z.object({
|
||||
count: z.number().optional(),
|
||||
from: z.string().optional(),
|
||||
limit: z.union([z.number(), def.common.strToInt]).optional(),
|
||||
before: z.string().optional(),
|
||||
after: z.string().optional(),
|
||||
reverse: def.common.strToBool.optional(),
|
||||
})
|
||||
export type ListRecordsParams = z.infer<typeof listRecordsParams>
|
||||
|
||||
export const listRecordsResponse = z.object({
|
||||
records: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
uri: z.string(),
|
||||
value: z.any(),
|
||||
}),
|
||||
),
|
||||
@ -43,7 +45,7 @@ export const listRecordsResponse = z.object({
|
||||
export type ListRecordsResponse = z.infer<typeof listRecordsResponse>
|
||||
|
||||
export const getRecordResponse = z.object({
|
||||
key: z.string(),
|
||||
uri: z.string(),
|
||||
value: z.any(),
|
||||
})
|
||||
export type GetRecordResponse = z.infer<typeof getRecordResponse>
|
||||
|
@ -248,7 +248,7 @@ class AdxRepoCollectionClient {
|
||||
async list(
|
||||
schema: t.SchemaOpt,
|
||||
params?: ht.ListRecordsParams,
|
||||
): Promise<ht.ListRecordsResponse> {
|
||||
): Promise<t.ListRecordsResponseValidated> {
|
||||
const url = this.repo.pds.url(
|
||||
PdsEndpoint.RepoCollection,
|
||||
[this.repo.did, this.id],
|
||||
@ -264,7 +264,7 @@ class AdxRepoCollectionClient {
|
||||
records: resSafe.records.map((record) => {
|
||||
const validation = validator.validate(record.value)
|
||||
return {
|
||||
key: record.key,
|
||||
uri: record.uri,
|
||||
value: record.value,
|
||||
valid: validation.valid,
|
||||
fullySupported: validation.fullySupported,
|
||||
@ -296,7 +296,7 @@ class AdxRepoCollectionClient {
|
||||
const validator = getRecordValidator(schema, this.repo.pds.client)
|
||||
const validation = validator.validate(resSafe.value)
|
||||
return {
|
||||
key: resSafe.key,
|
||||
uri: resSafe.uri,
|
||||
value: resSafe.value,
|
||||
valid: validation.valid,
|
||||
fullySupported: validation.fullySupported,
|
||||
@ -309,7 +309,11 @@ class AdxRepoCollectionClient {
|
||||
/**
|
||||
* Create a new record.
|
||||
*/
|
||||
async create(schema: t.SchemaOpt, value: any): Promise<AdxUri> {
|
||||
async create(
|
||||
schema: t.SchemaOpt,
|
||||
value: any,
|
||||
validate = true,
|
||||
): Promise<AdxUri> {
|
||||
if (!this.repo.writable) {
|
||||
throw new err.WritePermissionError()
|
||||
}
|
||||
@ -320,7 +324,7 @@ class AdxRepoCollectionClient {
|
||||
const url = this.repo.pds.url(
|
||||
PdsEndpoint.RepoCollection,
|
||||
[this.repo.did, this.id],
|
||||
{ verified: true },
|
||||
{ validate },
|
||||
)
|
||||
const res = await axios.post(url, value).catch(toAPIError)
|
||||
const { uri } = ht.createRecordResponse.parse(res.data)
|
||||
@ -330,7 +334,7 @@ class AdxRepoCollectionClient {
|
||||
/**
|
||||
* Write the record.
|
||||
*/
|
||||
async put(schema: t.SchemaOpt, key: string, value: any) {
|
||||
async put(schema: t.SchemaOpt, key: string, value: any, validate = true) {
|
||||
if (!this.repo.writable) {
|
||||
throw new err.WritePermissionError()
|
||||
}
|
||||
@ -341,7 +345,7 @@ class AdxRepoCollectionClient {
|
||||
const url = this.repo.pds.url(
|
||||
PdsEndpoint.RepoRecord,
|
||||
[this.repo.did, this.id, key],
|
||||
{ verified: true },
|
||||
{ validate },
|
||||
)
|
||||
const res = await axios.put(url, value).catch(toAPIError)
|
||||
const { uri } = ht.createRecordResponse.parse(res.data)
|
||||
|
@ -32,6 +32,10 @@ export interface GetRecordResponseValidated extends GetRecordResponse {
|
||||
fallbacks?: string[] | undefined
|
||||
}
|
||||
|
||||
export interface ListRecordsResponseValidated {
|
||||
records: GetRecordResponseValidated[]
|
||||
}
|
||||
|
||||
export interface BatchWrite {
|
||||
action: 'create' | 'put' | 'del'
|
||||
collection: string
|
||||
|
@ -38,6 +38,8 @@ const strToInt = z
|
||||
})
|
||||
.transform((str) => parseInt(str))
|
||||
|
||||
const strToBool = z.string().transform((str) => str === 'true' || str === 't')
|
||||
|
||||
export const def = {
|
||||
string: z.string(),
|
||||
cid,
|
||||
@ -45,4 +47,5 @@ export const def = {
|
||||
did,
|
||||
bytes,
|
||||
strToInt,
|
||||
strToBool,
|
||||
}
|
||||
|
@ -26,10 +26,24 @@ export class Collection {
|
||||
return this.repo.blockstore.getUnchecked(cid)
|
||||
}
|
||||
|
||||
async listRecords(count?: number): Promise<unknown[]> {
|
||||
const vals = await this.repo.data.listWithPrefix(this.name(), count)
|
||||
async listRecords(
|
||||
count?: number,
|
||||
after?: TID,
|
||||
before?: TID,
|
||||
): Promise<{ key: TID; value: unknown }[]> {
|
||||
count = count || Number.MAX_SAFE_INTEGER
|
||||
const afterKey = after ? this.dataIdForRecord(after) : this.name()
|
||||
const beforeKey = before ? this.dataIdForRecord(before) : this.name() + 'a'
|
||||
const vals = await this.repo.data.list(count, afterKey, beforeKey)
|
||||
return Promise.all(
|
||||
vals.map((val) => this.repo.blockstore.getUnchecked(val.value)),
|
||||
vals.map(async (val) => {
|
||||
const parts = val.key.split('/')
|
||||
const tid = TID.fromStr(parts[parts.length - 1])
|
||||
return {
|
||||
key: tid,
|
||||
value: await this.repo.blockstore.getUnchecked(val.value),
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -628,10 +628,12 @@ export class MST implements DataStore {
|
||||
}
|
||||
}
|
||||
|
||||
async list(from: string, count: number): Promise<Leaf[]> {
|
||||
async list(count: number, after?: string, before?: string): Promise<Leaf[]> {
|
||||
const vals: Leaf[] = []
|
||||
for await (const leaf of this.walkLeavesFrom(from)) {
|
||||
for await (const leaf of this.walkLeavesFrom(after || '')) {
|
||||
if (leaf.key === after) continue
|
||||
if (vals.length >= count) break
|
||||
if (before && leaf.key >= before) break
|
||||
vals.push(leaf)
|
||||
}
|
||||
return vals
|
||||
|
@ -76,7 +76,7 @@ export interface DataStore {
|
||||
update(key: string, value: CID): Promise<DataStore>
|
||||
delete(key: string): Promise<DataStore>
|
||||
get(key: string): Promise<CID | null>
|
||||
list(from: string, count: number): Promise<DataValue[]>
|
||||
list(count: number, after?: string, before?: string): Promise<DataValue[]>
|
||||
listWithPrefix(prefix: string, count?: number): Promise<DataValue[]>
|
||||
diff(other: DataStore): Promise<DataDiff>
|
||||
save(): Promise<CID>
|
||||
|
@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"build": "node ./build.js",
|
||||
"start": "node dist/index.js",
|
||||
"test": "jest tests/revamp.test.ts",
|
||||
"test": "jest tests/crud.test.ts",
|
||||
"prettier": "prettier --check src/",
|
||||
"prettier:fix": "prettier --write src/",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
|
@ -133,16 +133,17 @@ export class Database {
|
||||
}
|
||||
record.raw = JSON.stringify(obj)
|
||||
|
||||
const table = this.findTableForCollection(uri.collection)
|
||||
await table.set(uri, obj)
|
||||
const recordTable = this.db.getRepository(AdxRecord)
|
||||
await recordTable.save(record)
|
||||
|
||||
const table = this.findTableForCollection(uri.collection)
|
||||
await table.set(uri, obj)
|
||||
}
|
||||
|
||||
async deleteRecord(uri: AdxUri) {
|
||||
const table = this.findTableForCollection(uri.collection)
|
||||
const recordTable = this.db.getRepository(AdxRecord)
|
||||
await Promise.all([table.delete(uri), recordTable.delete(uri)])
|
||||
await Promise.all([table.delete(uri), recordTable.delete(uri.toString())])
|
||||
}
|
||||
|
||||
async listCollectionsForDid(did: string): Promise<string[]> {
|
||||
@ -160,32 +161,41 @@ export class Database {
|
||||
did: string,
|
||||
collection: string,
|
||||
limit: number,
|
||||
reverse: boolean,
|
||||
before?: string,
|
||||
): Promise<unknown[]> {
|
||||
after?: string,
|
||||
): Promise<{ uri: string; value: unknown }[]> {
|
||||
const builder = await this.db
|
||||
.createQueryBuilder()
|
||||
.select(['record.uri AS uri, record.raw AS raw'])
|
||||
.from(AdxRecord, 'record')
|
||||
.where('record.did = :did', { did })
|
||||
.andWhere('record.collection = :collection', { collection })
|
||||
.orderBy('record.tid', 'DESC')
|
||||
.orderBy('record.tid', reverse ? 'DESC' : 'ASC')
|
||||
.limit(limit)
|
||||
|
||||
if (before !== undefined) {
|
||||
builder.andWhere('record.tid <= :before', { before })
|
||||
builder.andWhere('record.tid < :before', { before })
|
||||
}
|
||||
if (after !== undefined) {
|
||||
builder.andWhere('record.tid > :after', { after })
|
||||
}
|
||||
|
||||
const res = await builder.getRawMany()
|
||||
return res.map((row) => {
|
||||
return {
|
||||
...JSON.parse(row.raw),
|
||||
uri: row.uri,
|
||||
value: JSON.parse(row.raw),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getRecord(uri: AdxUri): Promise<unknown | null> {
|
||||
const table = this.findTableForCollection(uri.collection)
|
||||
return table.get(uri)
|
||||
const record = await this.db
|
||||
.getRepository(AdxRecord)
|
||||
.findOneBy({ uri: uri.toString() })
|
||||
if (record === null) return null
|
||||
return JSON.parse(record.raw)
|
||||
}
|
||||
|
||||
findTableForCollection(collection: string) {
|
||||
|
@ -6,6 +6,8 @@ import {
|
||||
DescribeRepoResponse,
|
||||
listRecordsParams,
|
||||
batchWriteParams,
|
||||
ListRecordsResponse,
|
||||
GetRecordResponse,
|
||||
} from '@adxp/api'
|
||||
import { resolveName, AdxUri, BatchWrite, TID } from '@adxp/common'
|
||||
import * as auth from '@adxp/auth'
|
||||
@ -53,7 +55,7 @@ router.post('/:did', async (req, res) => {
|
||||
}
|
||||
}
|
||||
await db.setRepoRoot(did, repo.cid)
|
||||
res.status(200).send()
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
router.post('/:did/c/:namespace/:dataset', async (req, res) => {
|
||||
@ -86,7 +88,7 @@ router.post('/:did/c/:namespace/:dataset', async (req, res) => {
|
||||
}
|
||||
await db.setRepoRoot(did, repo.cid)
|
||||
// @TODO update subscribers
|
||||
res.status(200).send({ uri: uri.toString() })
|
||||
res.json({ uri: uri.toString() })
|
||||
})
|
||||
|
||||
router.put('/:did/c/:namespace/:dataset/r/:tid', async (req, res) => {
|
||||
@ -119,7 +121,7 @@ router.put('/:did/c/:namespace/:dataset/r/:tid', async (req, res) => {
|
||||
}
|
||||
await db.setRepoRoot(did, repo.cid)
|
||||
// @TODO update subscribers
|
||||
res.status(200).send({ uri: uri.toString() })
|
||||
res.json({ uri: uri.toString() })
|
||||
})
|
||||
|
||||
router.delete('/:did/c/:namespace/:dataset/r/:tid', async (req, res) => {
|
||||
@ -131,10 +133,10 @@ router.delete('/:did/c/:namespace/:dataset/r/:tid', async (req, res) => {
|
||||
const repo = await util.loadRepo(res, did, authStore)
|
||||
await repo.getCollection(collection).deleteRecord(TID.fromStr(tid))
|
||||
const uri = new AdxUri(`${did}/${collection}/${tid.toString()}`)
|
||||
await db.indexRecord(uri, req.body)
|
||||
await db.deleteRecord(uri)
|
||||
await db.setRepoRoot(did, repo.cid)
|
||||
// @TODO update subscribers
|
||||
res.status(200).send()
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
// DESCRIBE REPO
|
||||
@ -167,25 +169,44 @@ router.get('/:nameOrDid', async (req, res) => {
|
||||
let didDoc: didSdk.DIDDocument
|
||||
let nameIsCorrect: boolean | undefined
|
||||
|
||||
if (nameOrDid.startsWith('did:')) {
|
||||
did = nameOrDid
|
||||
didDoc = await resolveDidWrapped(did)
|
||||
name = 'undefined' // TODO: need to decide how username gets published in the did doc
|
||||
if (confirmName) {
|
||||
const namesDeclaredDid = await resolveNameWrapped(name)
|
||||
nameIsCorrect = did === namesDeclaredDid
|
||||
}
|
||||
} else {
|
||||
name = nameOrDid
|
||||
did = await resolveNameWrapped(name)
|
||||
didDoc = await resolveDidWrapped(did)
|
||||
if (confirmName) {
|
||||
const didsDeclaredName = 'undefined' // TODO: need to decide how username gets published in the did doc
|
||||
nameIsCorrect = name === didsDeclaredName
|
||||
}
|
||||
}
|
||||
// @TODO add back once we have a did network
|
||||
// if (nameOrDid.startsWith('did:')) {
|
||||
// did = nameOrDid
|
||||
// didDoc = await resolveDidWrapped(did)
|
||||
// name = 'undefined' // TODO: need to decide how username gets published in the did doc
|
||||
// if (confirmName) {
|
||||
// const namesDeclaredDid = await resolveNameWrapped(name)
|
||||
// nameIsCorrect = did === namesDeclaredDid
|
||||
// }
|
||||
// } else {
|
||||
// name = nameOrDid
|
||||
// did = await resolveNameWrapped(name)
|
||||
// didDoc = await resolveDidWrapped(did)
|
||||
// if (confirmName) {
|
||||
// const didsDeclaredName = 'undefined' // TODO: need to decide how username gets published in the did doc
|
||||
// nameIsCorrect = name === didsDeclaredName
|
||||
// }
|
||||
// }
|
||||
|
||||
const db = util.getDB(res)
|
||||
if (nameOrDid.startsWith('did:')) {
|
||||
did = nameOrDid
|
||||
const found = await db.getUsernameForDid(did)
|
||||
if (found === null) {
|
||||
throw new ServerError(404, 'Could not find username for did')
|
||||
}
|
||||
name = found
|
||||
} else {
|
||||
name = nameOrDid
|
||||
const found = await db.getDidForUsername(name)
|
||||
if (found === null) {
|
||||
throw new ServerError(404, 'Could not find did for username')
|
||||
}
|
||||
did = found
|
||||
}
|
||||
didDoc = {} as any
|
||||
nameIsCorrect = true
|
||||
|
||||
const collections = await db.listCollectionsForDid(did)
|
||||
|
||||
const resBody: DescribeRepoResponse = {
|
||||
@ -195,7 +216,6 @@ router.get('/:nameOrDid', async (req, res) => {
|
||||
collections,
|
||||
nameIsCorrect,
|
||||
}
|
||||
res.status(200)
|
||||
res.json(resBody)
|
||||
})
|
||||
|
||||
@ -205,14 +225,31 @@ router.get('/:nameOrDid', async (req, res) => {
|
||||
router.get('/:nameOrDid/c/:namespace/:dataset', async (req, res) => {
|
||||
const { nameOrDid, namespace, dataset } = req.params
|
||||
const coll = namespace + '/' + dataset
|
||||
const { count = 50, from } = util.checkReqBody(req.query, listRecordsParams)
|
||||
const {
|
||||
limit = 50,
|
||||
before,
|
||||
after,
|
||||
reverse = false,
|
||||
} = util.checkReqBody(req.query, listRecordsParams)
|
||||
|
||||
const db = util.getDB(res)
|
||||
const did = nameOrDid.startsWith('did:')
|
||||
? nameOrDid
|
||||
: await resolveNameWrapped(nameOrDid)
|
||||
: await db.getDidForUsername(nameOrDid)
|
||||
if (!did) {
|
||||
throw new ServerError(404, `Could not find did for ${nameOrDid}`)
|
||||
}
|
||||
|
||||
const records = await db.listRecordsForCollection(did, coll, count, from)
|
||||
res.status(200).send(records)
|
||||
const records = await db.listRecordsForCollection(
|
||||
did,
|
||||
coll,
|
||||
limit,
|
||||
reverse,
|
||||
before,
|
||||
after,
|
||||
)
|
||||
|
||||
res.json({ records } as ListRecordsResponse)
|
||||
})
|
||||
|
||||
// GET RECORD
|
||||
@ -232,7 +269,7 @@ router.get('/:nameOrDid/c/:namespace/:dataset/r/:tid', async (req, res) => {
|
||||
if (record === null) {
|
||||
throw new ServerError(404, `Could not locate record: ${uri}`)
|
||||
}
|
||||
res.status(200).send(record)
|
||||
res.json({ uri: uri.toString(), value: record } as GetRecordResponse)
|
||||
})
|
||||
|
||||
export default router
|
||||
|
256
packages/server/tests/crud.test.ts
Normal file
256
packages/server/tests/crud.test.ts
Normal file
@ -0,0 +1,256 @@
|
||||
import { AdxClient, AdxUri } from '@adxp/api'
|
||||
import { schemas } from '@adxp/microblog'
|
||||
|
||||
const url = 'http://localhost:2583'
|
||||
const adx = new AdxClient({
|
||||
pds: url,
|
||||
schemas: schemas,
|
||||
})
|
||||
const alice = { username: 'alice', did: 'did:example:alice' }
|
||||
const bob = { username: 'bob', did: 'did:example:bob' }
|
||||
|
||||
describe('crud operations', () => {
|
||||
it('registers users', async () => {
|
||||
await adx.mainPds.registerRepo(alice)
|
||||
await adx.mainPds.registerRepo(bob)
|
||||
})
|
||||
|
||||
it('describes repo', async () => {
|
||||
const description = await adx.mainPds.repo(alice.did).describe()
|
||||
expect(description.name).toBe(alice.username)
|
||||
expect(description.did).toBe(alice.did)
|
||||
const description2 = await adx.mainPds.repo(bob.did).describe()
|
||||
expect(description2.name).toBe(bob.username)
|
||||
expect(description2.did).toBe(bob.did)
|
||||
})
|
||||
|
||||
let uri: AdxUri
|
||||
it('creates records', async () => {
|
||||
uri = await adx.mainPds
|
||||
.repo(alice.did)
|
||||
.collection('bsky/posts')
|
||||
.create('*', {
|
||||
$type: 'blueskyweb.xyz:Post',
|
||||
text: 'Hello, world!',
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
expect(uri.toString()).toBe(
|
||||
`adx://${alice.did}/bsky/posts/${uri.recordKey}`,
|
||||
)
|
||||
})
|
||||
|
||||
it('lists records', async () => {
|
||||
const res = await adx.mainPds
|
||||
.repo(alice.did)
|
||||
.collection('bsky/posts')
|
||||
.list('*')
|
||||
expect(res.records.length).toBe(1)
|
||||
expect(res.records[0].uri).toBe(uri.toString())
|
||||
expect(res.records[0].value.text).toBe('Hello, world!')
|
||||
})
|
||||
|
||||
it('gets records', async () => {
|
||||
const res = await adx.mainPds
|
||||
.repo(alice.did)
|
||||
.collection('bsky/posts')
|
||||
.get('*', uri.recordKey)
|
||||
expect(res.uri).toBe(uri.toString())
|
||||
expect(res.value.text).toBe('Hello, world!')
|
||||
})
|
||||
|
||||
it('puts records', async () => {
|
||||
const res = await adx.mainPds
|
||||
.repo(alice.did)
|
||||
.collection('bsky/posts')
|
||||
.put('*', uri.recordKey, {
|
||||
$type: 'blueskyweb.xyz:Post',
|
||||
text: 'Hello, universe!',
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
expect(res.toString()).toBe(uri.toString())
|
||||
|
||||
const res2 = await adx.mainPds
|
||||
.repo(alice.did)
|
||||
.collection('bsky/posts')
|
||||
.get('*', uri.recordKey)
|
||||
expect(res2.uri).toBe(uri.toString())
|
||||
expect(res2.value.text).toBe('Hello, universe!')
|
||||
})
|
||||
|
||||
it('deletes records', async () => {
|
||||
await adx.mainPds
|
||||
.repo(alice.did)
|
||||
.collection('bsky/posts')
|
||||
.del(uri.recordKey)
|
||||
const res = await adx.mainPds
|
||||
.repo(alice.did)
|
||||
.collection('bsky/posts')
|
||||
.list('*')
|
||||
expect(res.records.length).toBe(0)
|
||||
})
|
||||
|
||||
it('lists records with pagination', async () => {
|
||||
const feed = adx.mainPds.repo(alice.did).collection('bsky/posts')
|
||||
const uri1 = await feed.create('Post', {
|
||||
$type: 'blueskyweb.xyz:Post',
|
||||
text: 'Post 1',
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
const uri2 = await feed.create('Post', {
|
||||
$type: 'blueskyweb.xyz:Post',
|
||||
text: 'Post 2',
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
const uri3 = await feed.create('Post', {
|
||||
$type: 'blueskyweb.xyz:Post',
|
||||
text: 'Post 3',
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
const uri4 = await feed.create('Post', {
|
||||
$type: 'blueskyweb.xyz:Post',
|
||||
text: 'Post 4',
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
{
|
||||
const list = await feed.list('Post', { limit: 2 })
|
||||
expect(list.records.length).toBe(2)
|
||||
expect(list.records[0].value.text).toBe('Post 1')
|
||||
expect(list.records[1].value.text).toBe('Post 2')
|
||||
}
|
||||
{
|
||||
const list = await feed.list('Post', { limit: 2, reverse: true })
|
||||
expect(list.records.length).toBe(2)
|
||||
expect(list.records[0].value.text).toBe('Post 4')
|
||||
expect(list.records[1].value.text).toBe('Post 3')
|
||||
}
|
||||
|
||||
{
|
||||
const list = await feed.list('Post', { after: uri2.recordKey })
|
||||
expect(list.records.length).toBe(2)
|
||||
expect(list.records[0].value.text).toBe('Post 3')
|
||||
expect(list.records[1].value.text).toBe('Post 4')
|
||||
}
|
||||
{
|
||||
const list = await feed.list('Post', { before: uri3.recordKey })
|
||||
expect(list.records.length).toBe(2)
|
||||
expect(list.records[0].value.text).toBe('Post 1')
|
||||
expect(list.records[1].value.text).toBe('Post 2')
|
||||
}
|
||||
{
|
||||
const list = await feed.list('Post', {
|
||||
before: uri4.recordKey,
|
||||
after: uri1.recordKey,
|
||||
})
|
||||
expect(list.records.length).toBe(2)
|
||||
expect(list.records[0].value.text).toBe('Post 2')
|
||||
expect(list.records[1].value.text).toBe('Post 3')
|
||||
}
|
||||
|
||||
await feed.del(uri1.recordKey)
|
||||
await feed.del(uri2.recordKey)
|
||||
await feed.del(uri3.recordKey)
|
||||
await feed.del(uri4.recordKey)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validation', () => {
|
||||
it('requires a $type on records', async () => {
|
||||
const feed = adx.mainPds.repo(alice.did).collection('bsky/posts')
|
||||
await expect(feed.create('Post', {})).rejects.toThrow(
|
||||
'The passed value does not declare a $type',
|
||||
)
|
||||
await expect(feed.put('Post', 'foo', {})).rejects.toThrow(
|
||||
'The passed value does not declare a $type',
|
||||
)
|
||||
})
|
||||
|
||||
it('requires the schema to be known', async () => {
|
||||
const feed = adx.mainPds.repo(alice.did).collection('bsky/posts')
|
||||
await expect(feed.list('Foobar')).rejects.toThrow(
|
||||
'Schema not found: Foobar',
|
||||
)
|
||||
await expect(feed.create('Foobar', {})).rejects.toThrow(
|
||||
'Schema not found: Foobar',
|
||||
)
|
||||
await expect(feed.put('Foobar', 'foo', {})).rejects.toThrow(
|
||||
'Schema not found: Foobar',
|
||||
)
|
||||
})
|
||||
|
||||
it('requires the $type to match the schema', async () => {
|
||||
const feed = adx.mainPds.repo(alice.did).collection('bsky/posts')
|
||||
await expect(
|
||||
feed.create('Post', { $type: 'blueskyweb.xyz:Like' }),
|
||||
).rejects.toThrow('Record type blueskyweb.xyz:Like is not supported')
|
||||
await expect(
|
||||
feed.put('Post', 'foo', { $type: 'blueskyweb.xyz:Like' }),
|
||||
).rejects.toThrow('Record type blueskyweb.xyz:Like is not supported')
|
||||
})
|
||||
|
||||
it('validates the record on write', async () => {
|
||||
const feed = adx.mainPds.repo(alice.did).collection('bsky/posts')
|
||||
await expect(
|
||||
feed.create('Post', { $type: 'blueskyweb.xyz:Post' }),
|
||||
).rejects.toThrow(
|
||||
"Failed blueskyweb.xyz:Post validation for #/required: must have required property 'text'",
|
||||
)
|
||||
await expect(
|
||||
feed.put('Post', 'foo', { $type: 'blueskyweb.xyz:Post' }),
|
||||
).rejects.toThrow(
|
||||
"Failed blueskyweb.xyz:Post validation for #/required: must have required property 'text'",
|
||||
)
|
||||
})
|
||||
|
||||
it('validates the record on read', async () => {
|
||||
const feed = adx.mainPds.repo(alice.did).collection('bsky/posts')
|
||||
const uri1 = await feed.create(
|
||||
'*',
|
||||
{
|
||||
$type: 'blueskyweb.xyz:Post',
|
||||
record: 'is bad',
|
||||
},
|
||||
false,
|
||||
)
|
||||
const uri2 = await feed.create(
|
||||
'*',
|
||||
{
|
||||
$type: 'blueskyweb.xyz:Unknown',
|
||||
dunno: 'lol',
|
||||
},
|
||||
false,
|
||||
)
|
||||
const res1 = await feed.list('Post')
|
||||
expect(res1.records[0].value.record).toBe('is bad')
|
||||
expect(res1.records[0].valid).toBeFalsy()
|
||||
expect(res1.records[0].fullySupported).toBeFalsy()
|
||||
expect(res1.records[0].compatible).toBeTruthy()
|
||||
expect(res1.records[0].error).toBe(
|
||||
`Failed blueskyweb.xyz:Post validation for #/required: must have required property 'text'`,
|
||||
)
|
||||
expect(res1.records[1].value.dunno).toBe('lol')
|
||||
expect(res1.records[1].valid).toBeFalsy()
|
||||
expect(res1.records[1].fullySupported).toBeFalsy()
|
||||
expect(res1.records[1].compatible).toBeFalsy()
|
||||
expect(res1.records[1].error).toBe(
|
||||
`Record type blueskyweb.xyz:Unknown is not supported`,
|
||||
)
|
||||
const res2 = await feed.get('Post', uri1.recordKey)
|
||||
expect(res2.value.record).toBe('is bad')
|
||||
expect(res2.valid).toBeFalsy()
|
||||
expect(res2.fullySupported).toBeFalsy()
|
||||
expect(res2.compatible).toBeTruthy()
|
||||
expect(res2.error).toBe(
|
||||
`Failed blueskyweb.xyz:Post validation for #/required: must have required property 'text'`,
|
||||
)
|
||||
const res3 = await feed.get('Post', uri2.recordKey)
|
||||
expect(res3.value.dunno).toBe('lol')
|
||||
expect(res3.valid).toBeFalsy()
|
||||
expect(res3.fullySupported).toBeFalsy()
|
||||
expect(res3.compatible).toBeFalsy()
|
||||
expect(res3.error).toBe(
|
||||
`Record type blueskyweb.xyz:Unknown is not supported`,
|
||||
)
|
||||
await feed.del(uri1.recordKey)
|
||||
await feed.del(uri2.recordKey)
|
||||
})
|
||||
})
|
@ -1,112 +0,0 @@
|
||||
import { MicroblogDelegator, Post, Like } from '@adxp/common'
|
||||
|
||||
import { CloseFn, newClient, runTestServer } from './_util'
|
||||
|
||||
const USE_TEST_SERVER = true
|
||||
|
||||
const PORT = 2583
|
||||
const HOST = `localhost:${PORT}`
|
||||
const SERVER_URL = `http://${HOST}`
|
||||
|
||||
describe('@TODO', () => {
|
||||
it('does not run tests', () => {
|
||||
expect(true)
|
||||
})
|
||||
})
|
||||
|
||||
// describe('delegator client', () => {
|
||||
// let alice: MicroblogDelegator
|
||||
// let bob: MicroblogDelegator
|
||||
|
||||
// let closeFn: CloseFn | undefined
|
||||
|
||||
// beforeAll(async () => {
|
||||
// if (USE_TEST_SERVER) {
|
||||
// closeFn = await runTestServer(PORT)
|
||||
// }
|
||||
// alice = await newClient(SERVER_URL)
|
||||
// bob = await newClient(SERVER_URL)
|
||||
// })
|
||||
|
||||
// afterAll(() => {
|
||||
// if (closeFn) {
|
||||
// closeFn()
|
||||
// }
|
||||
// })
|
||||
// it('works', () => {
|
||||
// expect(true)
|
||||
// })
|
||||
|
||||
// it('registers id', async () => {
|
||||
// await alice.register('alice')
|
||||
// await bob.register('bob')
|
||||
// })
|
||||
|
||||
// it('retrieves id', async () => {
|
||||
// const did = await alice.lookupDid(`alice@${HOST}`)
|
||||
// expect(did).toBe(alice.did)
|
||||
// })
|
||||
|
||||
// let post: Post
|
||||
// const postText = 'hello world!'
|
||||
|
||||
// it('creates post', async () => {
|
||||
// post = await alice.addPost(postText)
|
||||
// })
|
||||
|
||||
// it('gets post', async () => {
|
||||
// const got = await alice.getPost(post.tid)
|
||||
// expect(got?.text).toBe(postText)
|
||||
// })
|
||||
|
||||
// it('edits post', async () => {
|
||||
// const newText = 'howdy universe!'
|
||||
// await alice.editPost(post.tid, newText)
|
||||
|
||||
// const got = await alice.getPost(post.tid)
|
||||
// expect(got?.text).toBe(newText)
|
||||
// })
|
||||
|
||||
// let like: Like
|
||||
|
||||
// it('creates like', async () => {
|
||||
// like = await bob.likePost(alice.did, post.tid)
|
||||
// })
|
||||
|
||||
// it('lists likes', async () => {
|
||||
// const likes = await bob.listLikes(10)
|
||||
// expect(likes.length).toBe(1)
|
||||
// expect(likes[0].tid.toString()).toBe(like.tid.toString())
|
||||
// expect(likes[0].post_tid.toString()).toBe(post.tid.toString())
|
||||
// })
|
||||
|
||||
// it('deletes like', async () => {
|
||||
// await bob.deleteLike(like.tid)
|
||||
// const likes = await bob.listLikes(10)
|
||||
// expect(likes.length).toBe(0)
|
||||
// })
|
||||
|
||||
// it('deletes post', async () => {
|
||||
// await alice.deletePost(post.tid)
|
||||
// const got = await alice.getPost(post.tid)
|
||||
// expect(got).toBe(null)
|
||||
// })
|
||||
|
||||
// it('follows user', async () => {
|
||||
// // register bob
|
||||
// await alice.followUser(`bob@${HOST}`)
|
||||
// })
|
||||
|
||||
// it('lists follows', async () => {
|
||||
// const follows = await alice.listFollows()
|
||||
// expect(follows.length).toBe(1)
|
||||
// expect(follows[0].did).toBe(bob.did)
|
||||
// expect(follows[0].username).toBe(`bob@${HOST}`)
|
||||
// })
|
||||
|
||||
// it('unfollows user', async () => {
|
||||
// await alice.unfollowUser(bob.did)
|
||||
// const follows = await alice.listFollows()
|
||||
// expect(follows.length).toBe(0)
|
||||
// })
|
||||
// })
|
@ -1,122 +0,0 @@
|
||||
import { MicroblogDelegator, Post, TimelinePost } from '@adxp/common'
|
||||
import { CloseFn, newClient, runTestServer } from './_util'
|
||||
|
||||
const USE_TEST_SERVER = true
|
||||
|
||||
const PORT_ONE = USE_TEST_SERVER ? 2585 : 2583
|
||||
const HOST_ONE = `localhost:${PORT_ONE}`
|
||||
const SERVER_ONE = `http://${HOST_ONE}`
|
||||
const PORT_TWO = USE_TEST_SERVER ? 2586 : 2584
|
||||
const HOST_TWO = `localhost:${PORT_TWO}`
|
||||
const SERVER_TWO = `http://${HOST_TWO}`
|
||||
|
||||
process.env['DID_NETWORK_URL'] = `${SERVER_ONE}/did-network`
|
||||
|
||||
describe('@TODO', () => {
|
||||
it('does not run tests', () => {
|
||||
expect(true)
|
||||
})
|
||||
})
|
||||
|
||||
// describe('indexer', () => {
|
||||
// let alice: MicroblogDelegator
|
||||
// let bob: MicroblogDelegator
|
||||
// let carol: MicroblogDelegator
|
||||
// let dan: MicroblogDelegator
|
||||
|
||||
// let closeFn1, closeFn2: CloseFn | undefined
|
||||
|
||||
// beforeAll(async () => {
|
||||
// if (USE_TEST_SERVER) {
|
||||
// closeFn1 = await runTestServer(PORT_ONE)
|
||||
// closeFn2 = await runTestServer(PORT_TWO)
|
||||
// }
|
||||
// alice = await newClient(SERVER_ONE)
|
||||
// bob = await newClient(SERVER_ONE)
|
||||
// carol = await newClient(SERVER_TWO)
|
||||
// dan = await newClient(SERVER_TWO)
|
||||
// await alice.register('alice')
|
||||
// await bob.register('bob')
|
||||
// await carol.register('carol')
|
||||
// await dan.register('dan')
|
||||
// })
|
||||
|
||||
// afterAll(async () => {
|
||||
// if (closeFn1) {
|
||||
// await closeFn1()
|
||||
// }
|
||||
// if (closeFn2) {
|
||||
// await closeFn2()
|
||||
// }
|
||||
// })
|
||||
|
||||
// it('follow multiple users', async () => {
|
||||
// await alice.followUser(`bob@${HOST_ONE}`)
|
||||
// await alice.followUser(`carol@${HOST_TWO}`)
|
||||
// await alice.followUser(`dan@${HOST_TWO}`)
|
||||
// })
|
||||
|
||||
// it('populate the timeline', async () => {
|
||||
// await bob.addPost('one')
|
||||
// await bob.addPost('two')
|
||||
// await carol.addPost('three')
|
||||
// await dan.addPost('four')
|
||||
// await carol.addPost('five')
|
||||
// await bob.addPost('six')
|
||||
// await dan.addPost('seven')
|
||||
// await dan.addPost('eight')
|
||||
// await carol.addPost('nine')
|
||||
// await bob.addPost('ten')
|
||||
// })
|
||||
|
||||
// it('get timeline', async () => {
|
||||
// const timeline = await alice.retrieveTimeline(10)
|
||||
// const timelineText = timeline.map((p: TimelinePost) => p.text)
|
||||
// const expected = [
|
||||
// 'ten',
|
||||
// 'nine',
|
||||
// 'eight',
|
||||
// 'seven',
|
||||
// 'six',
|
||||
// 'five',
|
||||
// 'four',
|
||||
// 'three',
|
||||
// 'two',
|
||||
// 'one',
|
||||
// ]
|
||||
// expect(timelineText).toEqual(expected)
|
||||
// })
|
||||
|
||||
// it('get old timeline', async () => {
|
||||
// const timeline = await alice.retrieveTimeline(2)
|
||||
// const lastSeen = timeline[1].tid
|
||||
// const oldTimeline = await alice.retrieveTimeline(10, lastSeen)
|
||||
// const timelineText = oldTimeline.map((p: TimelinePost) => p.text)
|
||||
// const expected = [
|
||||
// 'eight',
|
||||
// 'seven',
|
||||
// 'six',
|
||||
// 'five',
|
||||
// 'four',
|
||||
// 'three',
|
||||
// 'two',
|
||||
// 'one',
|
||||
// ]
|
||||
// expect(timelineText).toEqual(expected)
|
||||
// })
|
||||
|
||||
// let post: Post
|
||||
|
||||
// it('get some likes on your post', async () => {
|
||||
// post = await alice.addPost('hello world!')
|
||||
// const tid = post.tid
|
||||
// await bob.likePost(alice.did, tid)
|
||||
// await carol.likePost(alice.did, tid)
|
||||
// await dan.likePost(alice.did, tid)
|
||||
// })
|
||||
|
||||
// it('count likes on post', async () => {
|
||||
// const count = await alice.likeCount(alice.did, post.tid)
|
||||
// expect(count).toBe(3)
|
||||
// })
|
||||
// })
|
Loading…
x
Reference in New Issue
Block a user