Skip to content

Commit

Permalink
feat: Curation table + endpoints (#279)
Browse files Browse the repository at this point in the history
* chore: Add migration and some code for Curations table

* chore: Add collection utils and  post curation handler

* chore: Add get requests

* fix: Curation static functions

* chore: Add Collection util tests

* chore: Add curation router to server

* chore: Add tests for curation router

* chore: Add coverage report

* chore: Query collections by authorized user

* fix: Github actions

* fix: Github actions

* fix: Github actions

* fix: Github actions

* refactor: Rename hasAccess function

* chore: Add access tests

* fix: Get Curations

* refactor: Format

* fix: Query

* fix: Add created and updated at

* fix: Fix stored date in db

* refactor: Store status in curation + fix tests

* feat: Patch curation endpoint

* fix: Rename data property for update curation

* fix: Update curation

* fix: Don't check with created_at but with pending status
  • Loading branch information
fzavalia committed Sep 30, 2021
1 parent 6af1234 commit 81ba327
Show file tree
Hide file tree
Showing 11 changed files with 817 additions and 0 deletions.
38 changes: 38 additions & 0 deletions migrations/1632149797868_create-curations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { MigrationBuilder } from 'node-pg-migrate'
import { Collection } from '../src/Collection'
import { Curation } from '../src/Curation'

const tableName = Curation.tableName
const curationStatus = 'curation_status'

export const up = (pgm: MigrationBuilder) => {
pgm.createType(curationStatus, ['pending', 'approved', 'rejected'])

pgm.createTable(
tableName,
{
id: { type: 'UUID', primaryKey: true, unique: true, notNull: true },
collection_id: { type: 'UUID', notNull: true },
status: { type: 'CURATION_STATUS', notNull: true },
created_at: { type: 'TIMESTAMP', notNull: true },
updated_at: { type: 'TIMESTAMP', notNull: true },
},
{
ifNotExists: true,
constraints: {
foreignKeys: {
references: Collection.tableName,
columns: 'collection_id',
onDelete: 'CASCADE',
},
},
}
)

pgm.createIndex(tableName, 'collection_id')
}

export const down = (pgm: MigrationBuilder) => {
pgm.dropTable(tableName, {})
pgm.dropType(curationStatus)
}
60 changes: 60 additions & 0 deletions src/Collection/util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Collection } from '.'
import { Bridge } from '../ethereum/api/Bridge'
import { collectionAPI } from '../ethereum/api/collection'
import { getMergedCollection } from './util'

describe('getMergedCollection', () => {
let sampleCollection: { id: string }

beforeEach(() => {
sampleCollection = {
id: 'collectionId',
}
})

describe('when the db collection can not be found', () => {
it('should resolve with an undefined collection and a not found status', async () => {
jest.spyOn(Collection, 'findOne').mockResolvedValueOnce(undefined)

const { collection, status } = await getMergedCollection(
sampleCollection.id
)

expect(status).toBe('not_found')
expect(collection).toBeUndefined()
})
})

describe('when the remote collection can not be found', () => {
it('should resolve with the db collection and an incomplete status', async () => {
jest.spyOn(Collection, 'findOne').mockResolvedValueOnce(sampleCollection)
jest.spyOn(collectionAPI, 'fetchCollection').mockResolvedValueOnce(null)

const { collection, status } = await getMergedCollection(
sampleCollection.id
)

expect(status).toBe('incomplete')
expect(collection).toStrictEqual(sampleCollection)
})
})

describe('when both the db and remote collection are obtained', () => {
it('should resolve with the merged collection and a complete status', async () => {
jest.spyOn(Collection, 'findOne').mockResolvedValueOnce(sampleCollection)

jest
.spyOn(collectionAPI, 'fetchCollection')
.mockResolvedValueOnce(sampleCollection as any)

jest
.spyOn(Bridge, 'mergeCollection')
.mockReturnValueOnce(sampleCollection as any)

const { collection, status } = await getMergedCollection('collectionId')

expect(status).toBe('complete')
expect(collection).toStrictEqual(sampleCollection)
})
})
})
68 changes: 68 additions & 0 deletions src/Collection/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Collection, CollectionAttributes } from '.'
import { Bridge } from '../ethereum/api/Bridge'
import { collectionAPI } from '../ethereum/api/collection'
import { CollectionFragment } from '../ethereum/api/fragments'

type GetMergedCollectionResult =
| { status: 'not_found'; collection: undefined }
| { status: 'incomplete' | 'complete'; collection: CollectionAttributes }

/**
* Will return a collection formed by merging the collection present in
* the database and the one found in the graph.
*
* The result will depend according to the availability of those collections.
*
* When the collection does not exist on the database:
* status = not_found
*
* When the collection does not exist on the graph:
* status = incomplete & collection = database collection
*
* When both collections are available:
* status = complete & collection = merged db and graph collection
*/
export const getMergedCollection = async (
id: string
): Promise<GetMergedCollectionResult> => {
const dbCollection = await getDBCollection(id)

if (!dbCollection) {
return {
status: 'not_found',
collection: undefined,
}
}

const remoteCollection = await getRemoteCollection(
dbCollection.contract_address
)

if (!remoteCollection) {
return {
status: 'incomplete',
collection: dbCollection,
}
}

const mergedCollection = mergeCollections(dbCollection, remoteCollection)

return {
status: 'complete',
collection: mergedCollection,
}
}

export const getDBCollection = (id: string) =>
Collection.findOne<CollectionAttributes>(id)

export const getRemoteCollection = async (contractAddress: string) =>
(await collectionAPI.fetchCollection(contractAddress)) || undefined

export const getRemoteCollections = async () =>
await collectionAPI.fetchCollections()

export const mergeCollections = (
db: CollectionAttributes,
remote: CollectionFragment
) => Bridge.mergeCollection(db, remote)
39 changes: 39 additions & 0 deletions src/Curation/Curation.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Model, raw, SQL } from 'decentraland-server'
import { CurationAttributes } from './Curation.types'

export class Curation extends Model<CurationAttributes> {
static tableName = 'curations'

static getAllLatestByCollection() {
return this.query<CurationAttributes>(SQL`
SELECT DISTINCT ON (collection_id) * FROM ${raw(this.tableName)} AS c1
WHERE created_at = (
SELECT MAX(created_at) FROM ${raw(this.tableName)} AS c2
WHERE c1.collection_id = c2.collection_id
)`)
}

static getAllLatestForCollections(collectionIds: string[]) {
return this.query<CurationAttributes>(SQL`
SELECT DISTINCT ON (collection_id) * FROM ${raw(this.tableName)} AS cu1
WHERE collection_id = ANY(${collectionIds})
AND created_at = (
SELECT max(created_at) FROM ${raw(this.tableName)} AS cu2
WHERE cu1.collection_id = cu2.collection_id
)`)
}

static async getLatestForCollection(
collectionId: string
): Promise<CurationAttributes | undefined> {
const query = SQL`
SELECT DISTINCT ON (collection_id) * FROM ${raw(this.tableName)}
WHERE collection_id = ${collectionId}
AND created_at = (
SELECT MAX(created_at)
FROM ${raw(this.tableName)}
)`

return (await this.query<CurationAttributes>(query))[0]
}
}
Loading

0 comments on commit 81ba327

Please sign in to comment.