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

Add release endpoints #696

Merged
merged 2 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion backend/src/connectors/v2/authorisation/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ModelDoc } from '../../../models/v2/Model.js'
import { ReleaseDoc } from '../../../models/v2/Release.js'
import { UserDoc } from '../../../models/v2/User.js'
import config from '../../../utils/v2/config.js'
import { SillyAuthorisationConnector } from './silly.js'
Expand All @@ -7,11 +8,23 @@ export const ModelAction = {
Create: 'create',
View: 'view',
} as const

export type ModelActionKeys = (typeof ModelAction)[keyof typeof ModelAction]

export const ReleaseAction = {
Create: 'create',
View: 'view',
Delete: 'delete',
}
export type ReleaseActionKeys = (typeof ReleaseAction)[keyof typeof ReleaseAction]

export abstract class BaseAuthorisationConnector {
abstract userModelAction(user: UserDoc, model: ModelDoc, action: ModelActionKeys): Promise<boolean>
abstract userReleaseAction(
user: UserDoc,
model: ModelDoc,
release: ReleaseDoc,
action: ReleaseActionKeys
): Promise<boolean>
abstract getEntities(user: UserDoc): Promise<Array<string>>
}

Expand Down
6 changes: 6 additions & 0 deletions backend/src/connectors/v2/authorisation/silly.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ModelDoc } from '../../../models/v2/Model.js'
import { ReleaseDoc } from '../../../models/v2/Release.js'
import { UserDoc } from '../../../models/v2/User.js'
import { toEntity } from '../../../utils/v2/entity.js'
import { BaseAuthorisationConnector, ModelActionKeys } from './index.js'
Expand All @@ -13,6 +14,11 @@ export class SillyAuthorisationConnector implements BaseAuthorisationConnector {
return true
}

async userReleaseAction(_user: UserDoc, _model: ModelDoc, _release: ReleaseDoc, _action: string): Promise<boolean> {
// With silly authorisation, every user can complete every action.
return true
}

async getEntities(user: UserDoc) {
return [toEntity('user', user.dn)]
}
Expand Down
1 change: 1 addition & 0 deletions backend/src/models/v2/Release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const ReleaseSchema = new Schema<ReleaseInterface>(
)

ReleaseSchema.plugin(MongooseDelete, { overrideMethods: 'all', deletedBy: true, deletedByType: Schema.Types.ObjectId })
ReleaseSchema.index({ modelId: 1, semver: 1 }, { unique: true })

const ReleaseModel = model<ReleaseInterface>('v2_Release', ReleaseSchema)

Expand Down
7 changes: 6 additions & 1 deletion backend/src/routes/v2/release/deleteRelease.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import bodyParser from 'body-parser'
import { Request, Response } from 'express'
import { z } from 'zod'

import { deleteRelease as deleteReleaseService } from '../../../services/v2/release.js'
import { parse } from '../../../utils/validate.js'

export const deleteReleaseSchema = z.object({
Expand All @@ -18,7 +19,11 @@ interface DeleteReleaseResponse {
export const deleteRelease = [
bodyParser.json(),
async (req: Request, res: Response<DeleteReleaseResponse>) => {
const _ = parse(req, deleteReleaseSchema)
const {
params: { modelId, semver },
} = parse(req, deleteReleaseSchema)

await deleteReleaseService(req.user, modelId, semver)

return res.json({
message: 'Successfully removed release.',
Expand Down
27 changes: 7 additions & 20 deletions backend/src/routes/v2/release/getRelease.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Request, Response } from 'express'
import { z } from 'zod'

import { ReleaseInterface } from '../../../models/v2/Release.js'
import { getReleaseBySemver } from '../../../services/v2/release.js'
import { parse } from '../../../utils/validate.js'

export const getReleaseSchema = z.object({
Expand All @@ -19,28 +20,14 @@ interface getReleaseResponse {
export const getRelease = [
bodyParser.json(),
async (req: Request, res: Response<getReleaseResponse>) => {
const _ = parse(req, getReleaseSchema)
const {
params: { modelId, semver },
} = parse(req, getReleaseSchema)

return res.json({
release: {
modelId: 'example-model-1',
modelCardVersion: 14,

name: 'Example Release 1',
semver: '1.2.3',
notes: 'This is an example release',

minor: true,
draft: true,
const release = await getReleaseBySemver(req.user, modelId, semver)

files: ['example-file-id'],
images: ['example-image-id'],

deleted: false,

createdAt: new Date(),
updatedAt: new Date(),
},
return res.json({
release,
})
},
]
48 changes: 7 additions & 41 deletions backend/src/routes/v2/release/getReleases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Request, Response } from 'express'
import { z } from 'zod'

import { ReleaseInterface } from '../../../models/v2/Release.js'
import { getModelReleases } from '../../../services/v2/release.js'
import { parse } from '../../../utils/validate.js'

export const getReleasesSchema = z.object({
Expand All @@ -20,49 +21,14 @@ interface getReleasesResponse {
export const getReleases = [
bodyParser.json(),
async (req: Request, res: Response<getReleasesResponse>) => {
const _ = parse(req, getReleasesSchema)
const {
params: { modelId },
} = parse(req, getReleasesSchema)

return res.json({
releases: [
{
modelId: 'example-model-1',
modelCardVersion: 14,

name: 'Example Release 1',
semver: '1.2.2',
notes: 'This is an example release',

minor: true,
draft: true,

files: ['example-file-id'],
images: ['example-image-id'],

deleted: false,

createdAt: new Date(),
updatedAt: new Date(),
},
{
modelId: 'example-model-1',
modelCardVersion: 15,
const releases = await getModelReleases(req.user, modelId)

name: 'Example Release 2',
semver: '1.2.3',
notes: 'This is an example release',

minor: true,
draft: true,

files: ['example-file-id'],
images: ['example-image-id'],

deleted: false,

createdAt: new Date(),
updatedAt: new Date(),
},
],
return res.json({
releases,
})
},
]
28 changes: 8 additions & 20 deletions backend/src/routes/v2/release/postRelease.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Request, Response } from 'express'
import { z } from 'zod'

import { ReleaseInterface } from '../../../models/v2/Release.js'
import { createRelease } from '../../../services/v2/release.js'
import { parse } from '../../../utils/validate.js'

export const postReleaseSchema = z.object({
Expand Down Expand Up @@ -33,28 +34,15 @@ interface PostReleaseResponse {
export const postRelease = [
bodyParser.json(),
async (req: Request, res: Response<PostReleaseResponse>) => {
const _ = parse(req, postReleaseSchema)
const {
params: { modelId },
body,
} = parse(req, postReleaseSchema)

return res.json({
release: {
modelId: 'example-model-1',
modelCardVersion: 55,

name: 'Example Release 1',
semver: '1.2.3',
notes: 'This is an example release',

minor: true,
draft: true,
const release = await createRelease(req.user, { modelId, ...body })

files: ['file-id'],
images: ['image-id'],

deleted: false,

createdAt: new Date(),
updatedAt: new Date(),
},
return res.json({
release,
})
},
]
74 changes: 74 additions & 0 deletions backend/src/services/v2/release.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import authorisation, { ModelAction, ReleaseAction } from '../../connectors/v2/authorisation/index.js'
import { ModelInterface } from '../../models/v2/Model.js'
import Release, { ReleaseDoc, ReleaseInterface } from '../../models/v2/Release.js'
import { UserDoc } from '../../models/v2/User.js'
import { asyncFilter } from '../../utils/v2/array.js'
import { Forbidden, NotFound } from '../../utils/v2/error.js'
import { getModelById } from './model.js'

export type CreateReleaseParams = Pick<
ReleaseInterface,
'modelId' | 'modelCardVersion' | 'name' | 'semver' | 'notes' | 'minor' | 'draft' | 'files' | 'images'
>
export async function createRelease(user: UserDoc, releaseParams: CreateReleaseParams) {
const model = await getModelById(user, releaseParams.modelId)

const release = new Release({
...releaseParams,
})

if (!(await authorisation.userReleaseAction(user, model, release, ReleaseAction.Create))) {
throw Forbidden(`You do not have permission to create a release on this model.`, {
userDn: user.dn,
modelId: releaseParams.modelId,
})
}

await release.save()

return release
}

export async function getModelReleases(
user: UserDoc,
modelId: string
): Promise<Array<ReleaseDoc & { model: ModelInterface }>> {
const results = await Release.aggregate()
.match({ modelId })
.sort({ updatedAt: -1 })
.lookup({ from: 'v2_models', localField: 'modelId', foreignField: 'id', as: 'model' })
.append({ $set: { model: { $arrayElemAt: ['$model', 0] } } })

return asyncFilter(results, (result) => authorisation.userReleaseAction(user, result, result.model, ModelAction.View))
}

export async function getReleaseBySemver(user: UserDoc, modelId: string, semver: string) {
const model = await getModelById(user, modelId)
const release = await Release.findOne({
modelId,
semver,
})

if (!release) {
throw NotFound(`The requested release was not found.`, { modelId, semver })
}

if (!(await authorisation.userReleaseAction(user, model, release, ReleaseAction.View))) {
throw Forbidden(`You do not have permission to view this release.`, { userDn: user.dn })
}

return release
}

export async function deleteRelease(user: UserDoc, modelId: string, semver: string) {
const model = await getModelById(user, modelId)
const release = await getReleaseBySemver(user, modelId, semver)

if (!(await authorisation.userReleaseAction(user, model, release, ReleaseAction.Delete))) {
throw Forbidden(`You do not have permission to delete this release.`, { userDn: user.dn })
}

await release.delete()

return { modelId, semver }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`routes > release > deleteRelease > 200 > ok 1`] = `
{
"message": "Successfully removed release.",
}
`;

exports[`routes > release > getRelease > 200 > ok 1`] = `
{
"release": {
"_id": "test",
},
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`routes > release > getRelease > 200 > ok 1`] = `
{
"release": {
"_id": "test",
},
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`routes > release > getReleases > 200 > ok 1`] = `
{
"releases": [
{
"_id": "test",
},
],
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`routes > release > postRelease > 200 > ok 1`] = `
{
"release": {
"_id": "test",
},
}
`;

exports[`routes > release > postRelease > 400 > no description 1`] = `
{
"error": {
"context": {
"errors": [
{
"code": "invalid_type",
"expected": "string",
"message": "Required",
"path": [
"body",
"name",
],
"received": "undefined",
},
],
},
"message": "Required",
"name": "Error",
},
}
`;

exports[`routes > release > postRelease > 400 > no name 1`] = `
{
"error": {
"context": {
"errors": [
{
"code": "invalid_type",
"expected": "string",
"message": "Required",
"path": [
"body",
"name",
],
"received": "undefined",
},
],
},
"message": "Required",
"name": "Error",
},
}
`;
Loading