-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Curation table + endpoints (#279)
* 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
Showing
11 changed files
with
817 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} | ||
} |
Oops, something went wrong.