Skip to content
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
144 changes: 139 additions & 5 deletions src/__snapshots__/app.e2e.snap
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,69 @@ exports[`e2e:app > GET / > should respond with api documentation (json) 1`] = `
]
}
},
"/accounts/{uid}": {
"delete": {
"operationId": "accounts-delete",
"parameters": [
{
"name": "uid",
"required": true,
"in": "path",
"description": "id of account to delete",
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidCredentialException"
}
}
}
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MissingAccountException"
}
}
}
},
"500": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InternalServerException"
}
}
}
}
},
"security": [
{
"jwt": []
}
],
"tags": [
"accounts"
]
}
},
"/accounts/whoami": {
"get": {
"operationId": "accounts-whoami",
"parameters": [
{
"name": "authorization",
"in": "header",
"description": "bearer auth token",
"required": false,
"examples": {
"bearer": {
"value": "bearer <token>"
}
},
"schema": {
"type": "string"
}
Expand Down Expand Up @@ -123,6 +173,14 @@ exports[`e2e:app > GET / > should respond with api documentation (json) 1`] = `
}
},
"components": {
"securitySchemes": {
"bearer": {
"scheme": "bearer",
"bearerFormat": "JWT",
"name": "jwt",
"type": "http"
}
},
"schemas": {
"AccountCreatedPayload": {
"type": "object",
Expand Down Expand Up @@ -251,6 +309,82 @@ exports[`e2e:app > GET / > should respond with api documentation (json) 1`] = `
"reason"
]
},
"InvalidCredentialException": {
"type": "object",
"properties": {
"code": {
"type": "number",
"description": "http response status code",
"enum": [
401
]
},
"id": {
"type": "string",
"description": "unique id representing the exception",
"enum": [
"accounts/invalid-credential"
]
},
"message": {
"type": "string",
"description": "human-readable description of the exception"
},
"reason": {
"type": "null"
}
},
"required": [
"code",
"id",
"message",
"reason"
]
},
"MissingAccount": {
"type": "object",
"properties": {
"uid": {
"type": "string",
"description": "id of missing account"
}
},
"required": [
"uid"
]
},
"MissingAccountException": {
"type": "object",
"properties": {
"code": {
"type": "number",
"description": "http response status code",
"enum": [
404
]
},
"id": {
"type": "string",
"description": "unique id representing the exception",
"enum": [
"accounts/not-found"
]
},
"message": {
"type": "string",
"description": "human-readable description of the exception"
},
"reason": {
"$ref": "#/components/schemas/MissingAccount"
}
},
"required": [
"code",
"id",
"message",
"reason"
]
},
"ValidationException": {
"type": "object",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,32 @@ describe('functional:database/providers/Repository', () => {
})
})

describe('#delete', () => {
let record: IDocument
let seeder: Seeder
let subject: TestSubject

afterAll(async () => {
await seeder.down()
})

beforeAll(async () => {
subject = new TestSubject(mapper)
seeder = await new Seeder(factory, subject).up(1)
record = seeder.seeds[0]!
})

it('should return entity representing deleted record', async () => {
// Act
const result = await subject.delete(record._id)

// Expect
expect(result).to.be.instanceof(Entity)
expect(result).to.have.property('uid', String(record._id))
expect(subject).to.have.nested.property('store.size', 0)
})
})

describe('#entities', () => {
let count: number
let seeder: Seeder
Expand All @@ -62,20 +88,43 @@ describe('functional:database/providers/Repository', () => {
})
})

describe('#insert', () => {
let entity: Entity
let has: (store: Map<string, IDocument>) => boolean
describe('#findById', () => {
let record: IDocument
let seeder: Seeder
let subject: TestSubject

afterEach(async () => {
afterAll(async () => {
await seeder.down()
})

beforeAll(async () => {
subject = new TestSubject(mapper)
seeder = await new Seeder(factory, subject).up()
record = seeder.seeds[3]!
})

it('should return `null` if matching entity is not found', async () => {
expect(await subject.findById(new ObjectId())).to.be.null
})

it('should return entity representing matched record', async () => {
// Act
const result = await subject.findById(record._id)

// Expect
expect(result).to.be.instanceof(Entity)
expect(result).to.have.property('uid', String(record._id))
})
})

describe('#insert', () => {
let entity: Entity
let has: (store: Map<string, IDocument>) => boolean
let subject: TestSubject

beforeAll(() => {
entity = mapper.toDomain(factory.makeOne())
subject = new TestSubject(mapper)
seeder = new Seeder(factory, subject)

/**
* Check if `store` contains a record for {@linkcode entity}.
Expand Down
59 changes: 57 additions & 2 deletions src/database/providers/base.repository.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
* @module sneusers/database/providers/Repository
*/

import type { DatabaseRecord, Entity,
Mapper } from '@flex-development/sneusers/database'
import type {
DatabaseRecord,
Entity,
Mapper
} from '@flex-development/sneusers/database'
import type { ObjectId } from 'bson'
import { ok } from 'devlop'

/**
* Database repository model.
Expand Down Expand Up @@ -62,6 +66,57 @@ class Repository<T extends Entity = Entity> {
return [...this.store.values()]
}

/**
* Delete a record by `uid`.
*
* @public
* @instance
* @async
*
* @param {ObjectId | string} uid
* The id of the record to remove
* @return {Promise<ObjectId>}
* An entity representing the deleted record
*/
public async delete(uid: ObjectId | string): Promise<T> {
/**
* The entity to remove.
*
* @const {T | null} entity
*/
const entity: T | null = await this.findById(uid)

ok(entity, 'expected `entity` to remove')
this.store.delete(entity.uid)

return entity
}

/**
* Retrieve a record by `uid`.
*
* @public
* @instance
* @async
*
* @param {ObjectId | string} uid
* The id of the record to find
* @return {Promise<T | null>}
* An entity representing the matched record or `null`
*/
public async findById(uid: ObjectId | string): Promise<T | null> {
return new Promise(resolve => {
/**
* The matching record.
*
* @const {DatabaseRecord<T> | undefined} record
*/
const record: DatabaseRecord<T> | undefined = this.store.get(String(uid))

return void resolve(record ? this.mapper.toDomain(record) : null)
})
}

/**
* Add a new record.
*
Expand Down
1 change: 1 addition & 0 deletions src/enums/subroutes.mts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/
const enum subroutes {
ACCOUNTS_CREATE = '',
ACCOUNTS_UID = '/:uid',
ACCOUNTS_WHOAMI = '/whoami'
}

Expand Down
2 changes: 2 additions & 0 deletions src/errors/enums/exception-id.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
enum ExceptionId {
EMAIL_CONFLICT = 'accounts/email-conflict',
INTERNAL_SERVER_ERROR = 'sneusers/internal-error',
INVALID_CREDENTIAL = 'accounts/invalid-credential',
MISSING_ACCOUNT = 'accounts/not-found',
VALIDATION_FAILURE = 'sneusers/validation-failure'
}

Expand Down
3 changes: 3 additions & 0 deletions src/hooks/use-swagger.hook.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @module sneusers/hooks/useSwagger
*/

import AuthStrategy from '#enums/auth-strategy'
import routes from '#enums/routes'
import pkg from '@flex-development/sneusers/package.json' with { type: 'json' }
import {
Expand Down Expand Up @@ -47,6 +48,8 @@ function useSwagger(this: void, app: INestApplication): undefined {
docs.setTitle(pkg.openapi.title)
docs.setDescription(pkg.openapi.description)

docs.addBearerAuth({ name: AuthStrategy.JWT, type: 'http' })

return void SwaggerModule.setup(
routes.APP,
app,
Expand Down
Loading