Skip to content

Commit 0eaf650

Browse files
committed
feat(accounts): DELETE /accounts/:uid
Signed-off-by: Lexus Drumgold <[email protected]>
1 parent 15093f7 commit 0eaf650

37 files changed

+1378
-42
lines changed

src/__snapshots__/app.e2e.snap

Lines changed: 139 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,69 @@ exports[`e2e:app > GET / > should respond with api documentation (json) 1`] = `
6969
]
7070
}
7171
},
72+
"/accounts/{uid}": {
73+
"delete": {
74+
"operationId": "accounts-delete",
75+
"parameters": [
76+
{
77+
"name": "uid",
78+
"required": true,
79+
"in": "path",
80+
"description": "id of account to delete",
81+
"schema": {
82+
"type": "string"
83+
}
84+
}
85+
],
86+
"responses": {
87+
"204": {},
88+
"401": {
89+
"content": {
90+
"application/json": {
91+
"schema": {
92+
"$ref": "#/components/schemas/InvalidCredentialException"
93+
}
94+
}
95+
}
96+
},
97+
"404": {
98+
"content": {
99+
"application/json": {
100+
"schema": {
101+
"$ref": "#/components/schemas/MissingAccountException"
102+
}
103+
}
104+
}
105+
},
106+
"500": {
107+
"content": {
108+
"application/json": {
109+
"schema": {
110+
"$ref": "#/components/schemas/InternalServerException"
111+
}
112+
}
113+
}
114+
}
115+
},
116+
"security": [
117+
{
118+
"jwt": []
119+
}
120+
],
121+
"tags": [
122+
"accounts"
123+
]
124+
}
125+
},
72126
"/accounts/whoami": {
73127
"get": {
74128
"operationId": "accounts-whoami",
75129
"parameters": [
76130
{
77131
"name": "authorization",
78132
"in": "header",
133+
"description": "bearer auth token",
79134
"required": false,
80-
"examples": {
81-
"bearer": {
82-
"value": "bearer <token>"
83-
}
84-
},
85135
"schema": {
86136
"type": "string"
87137
}
@@ -123,6 +173,14 @@ exports[`e2e:app > GET / > should respond with api documentation (json) 1`] = `
123173
}
124174
},
125175
"components": {
176+
"securitySchemes": {
177+
"bearer": {
178+
"scheme": "bearer",
179+
"bearerFormat": "JWT",
180+
"name": "jwt",
181+
"type": "http"
182+
}
183+
},
126184
"schemas": {
127185
"AccountCreatedPayload": {
128186
"type": "object",
@@ -251,6 +309,82 @@ exports[`e2e:app > GET / > should respond with api documentation (json) 1`] = `
251309
"reason"
252310
]
253311
},
312+
"InvalidCredentialException": {
313+
"type": "object",
314+
"properties": {
315+
"code": {
316+
"type": "number",
317+
"description": "http response status code",
318+
"enum": [
319+
401
320+
]
321+
},
322+
"id": {
323+
"type": "string",
324+
"description": "unique id representing the exception",
325+
"enum": [
326+
"accounts/invalid-credential"
327+
]
328+
},
329+
"message": {
330+
"type": "string",
331+
"description": "human-readable description of the exception"
332+
},
333+
"reason": {
334+
"type": "null"
335+
}
336+
},
337+
"required": [
338+
"code",
339+
"id",
340+
"message",
341+
"reason"
342+
]
343+
},
344+
"MissingAccount": {
345+
"type": "object",
346+
"properties": {
347+
"uid": {
348+
"type": "string",
349+
"description": "id of missing account"
350+
}
351+
},
352+
"required": [
353+
"uid"
354+
]
355+
},
356+
"MissingAccountException": {
357+
"type": "object",
358+
"properties": {
359+
"code": {
360+
"type": "number",
361+
"description": "http response status code",
362+
"enum": [
363+
404
364+
]
365+
},
366+
"id": {
367+
"type": "string",
368+
"description": "unique id representing the exception",
369+
"enum": [
370+
"accounts/not-found"
371+
]
372+
},
373+
"message": {
374+
"type": "string",
375+
"description": "human-readable description of the exception"
376+
},
377+
"reason": {
378+
"$ref": "#/components/schemas/MissingAccount"
379+
}
380+
},
381+
"required": [
382+
"code",
383+
"id",
384+
"message",
385+
"reason"
386+
]
387+
},
254388
"ValidationException": {
255389
"type": "object",
256390
"properties": {

src/database/providers/__tests__/base.repository.functional.spec.mts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,32 @@ describe('functional:database/providers/Repository', () => {
3636
})
3737
})
3838

39+
describe('#delete', () => {
40+
let record: IDocument
41+
let seeder: Seeder
42+
let subject: TestSubject
43+
44+
afterAll(async () => {
45+
await seeder.down()
46+
})
47+
48+
beforeAll(async () => {
49+
subject = new TestSubject(mapper)
50+
seeder = await new Seeder(factory, subject).up(1)
51+
record = seeder.seeds[0]!
52+
})
53+
54+
it('should return entity representing deleted record', async () => {
55+
// Act
56+
const result = await subject.delete(record._id)
57+
58+
// Expect
59+
expect(result).to.be.instanceof(Entity)
60+
expect(result).to.have.property('uid', String(record._id))
61+
expect(subject).to.have.nested.property('store.size', 0)
62+
})
63+
})
64+
3965
describe('#entities', () => {
4066
let count: number
4167
let seeder: Seeder
@@ -62,20 +88,43 @@ describe('functional:database/providers/Repository', () => {
6288
})
6389
})
6490

65-
describe('#insert', () => {
66-
let entity: Entity
67-
let has: (store: Map<string, IDocument>) => boolean
91+
describe('#findById', () => {
92+
let record: IDocument
6893
let seeder: Seeder
6994
let subject: TestSubject
7095

71-
afterEach(async () => {
96+
afterAll(async () => {
7297
await seeder.down()
7398
})
7499

100+
beforeAll(async () => {
101+
subject = new TestSubject(mapper)
102+
seeder = await new Seeder(factory, subject).up()
103+
record = seeder.seeds[3]!
104+
})
105+
106+
it('should return `null` if matching entity is not found', async () => {
107+
expect(await subject.findById(new ObjectId())).to.be.null
108+
})
109+
110+
it('should return entity representing matched record', async () => {
111+
// Act
112+
const result = await subject.findById(record._id)
113+
114+
// Expect
115+
expect(result).to.be.instanceof(Entity)
116+
expect(result).to.have.property('uid', String(record._id))
117+
})
118+
})
119+
120+
describe('#insert', () => {
121+
let entity: Entity
122+
let has: (store: Map<string, IDocument>) => boolean
123+
let subject: TestSubject
124+
75125
beforeAll(() => {
76126
entity = mapper.toDomain(factory.makeOne())
77127
subject = new TestSubject(mapper)
78-
seeder = new Seeder(factory, subject)
79128

80129
/**
81130
* Check if `store` contains a record for {@linkcode entity}.

src/database/providers/base.repository.mts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
* @module sneusers/database/providers/Repository
44
*/
55

6-
import type { DatabaseRecord, Entity,
7-
Mapper } from '@flex-development/sneusers/database'
6+
import type {
7+
DatabaseRecord,
8+
Entity,
9+
Mapper
10+
} from '@flex-development/sneusers/database'
811
import type { ObjectId } from 'bson'
12+
import { ok } from 'devlop'
913

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

69+
/**
70+
* Delete a record by `uid`.
71+
*
72+
* @public
73+
* @instance
74+
* @async
75+
*
76+
* @param {ObjectId | string} uid
77+
* The id of the record to remove
78+
* @return {Promise<ObjectId>}
79+
* An entity representing the deleted record
80+
*/
81+
public async delete(uid: ObjectId | string): Promise<T> {
82+
/**
83+
* The entity to remove.
84+
*
85+
* @const {T | null} entity
86+
*/
87+
const entity: T | null = await this.findById(uid)
88+
89+
ok(entity, 'expected `entity` to remove')
90+
this.store.delete(entity.uid)
91+
92+
return entity
93+
}
94+
95+
/**
96+
* Retrieve a record by `uid`.
97+
*
98+
* @public
99+
* @instance
100+
* @async
101+
*
102+
* @param {ObjectId | string} uid
103+
* The id of the record to find
104+
* @return {Promise<T | null>}
105+
* An entity representing the matched record or `null`
106+
*/
107+
public async findById(uid: ObjectId | string): Promise<T | null> {
108+
return new Promise(resolve => {
109+
/**
110+
* The matching record.
111+
*
112+
* @const {DatabaseRecord<T> | undefined} record
113+
*/
114+
const record: DatabaseRecord<T> | undefined = this.store.get(String(uid))
115+
116+
return void resolve(record ? this.mapper.toDomain(record) : null)
117+
})
118+
}
119+
65120
/**
66121
* Add a new record.
67122
*

src/enums/subroutes.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111
const enum subroutes {
1212
ACCOUNTS_CREATE = '',
13+
ACCOUNTS_UID = '/:uid',
1314
ACCOUNTS_WHOAMI = '/whoami'
1415
}
1516

src/errors/enums/exception-id.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ enum ExceptionId {
1212
EMAIL_CONFLICT = 'accounts/email-conflict',
1313
INTERNAL_SERVER_ERROR = 'sneusers/internal-error',
1414
INVALID_CREDENTIAL = 'accounts/invalid-credential',
15+
MISSING_ACCOUNT = 'accounts/not-found',
1516
VALIDATION_FAILURE = 'sneusers/validation-failure'
1617
}
1718

src/hooks/use-swagger.hook.mts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* @module sneusers/hooks/useSwagger
44
*/
55

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

51+
docs.addBearerAuth({ name: AuthStrategy.JWT, type: 'http' })
52+
5053
return void SwaggerModule.setup(
5154
routes.APP,
5255
app,

0 commit comments

Comments
 (0)