Ver código fonte

Set up code for backend

Scaffold backend with Fastify CLI and replace Tap with Jest for
consistency.
master
TheoryOfNekomata 3 anos atrás
pai
commit
3d7dbd4439
36 arquivos alterados com 15013 adições e 11 exclusões
  1. +5
    -0
      REQUIREMENTS.md
  2. +0
    -11
      packages/app-web/src/models/Ringtone.ts
  3. +4
    -0
      packages/library-common/.gitignore
  4. +21
    -0
      packages/library-common/LICENSE
  5. +1
    -0
      packages/library-common/README.md
  6. +54
    -0
      packages/library-common/package.json
  7. +25
    -0
      packages/library-common/src/index.ts
  8. +35
    -0
      packages/library-common/tsconfig.json
  9. +8683
    -0
      packages/library-common/yarn.lock
  10. +67
    -0
      packages/service-core/.gitignore
  11. +1
    -0
      packages/service-core/README.md
  12. +1
    -0
      packages/service-core/global.d.ts
  13. +14
    -0
      packages/service-core/jest.config.js
  14. +0
    -0
      packages/service-core/jest.setup.ts
  15. +45
    -0
      packages/service-core/package.json
  16. +40
    -0
      packages/service-core/src/app.ts
  17. +10
    -0
      packages/service-core/src/modules/composer/service.ts
  18. +143
    -0
      packages/service-core/src/modules/ringtone/controller.ts
  19. +17
    -0
      packages/service-core/src/modules/ringtone/index.ts
  20. +261
    -0
      packages/service-core/src/modules/ringtone/repository.ts
  21. +1
    -0
      packages/service-core/src/modules/ringtone/response.ts
  22. +7
    -0
      packages/service-core/src/modules/ringtone/service.test.ts
  23. +61
    -0
      packages/service-core/src/modules/ringtone/service.ts
  24. +13
    -0
      packages/service-core/src/plugins/sensible.ts
  25. +16
    -0
      packages/service-core/src/routes/api/ringtones/index.ts
  26. +10
    -0
      packages/service-core/src/routes/api/search/ringtones/index.ts
  27. +9
    -0
      packages/service-core/src/routes/example/index.ts
  28. +9
    -0
      packages/service-core/src/routes/root.ts
  29. +111
    -0
      packages/service-core/src/utils/database/index.ts
  30. +4
    -0
      packages/service-core/src/utils/helpers/index.ts
  31. +22
    -0
      packages/service-core/test/helper.ts
  32. +83
    -0
      packages/service-core/test/mocks/repositories/Ringtone.ts
  33. +87
    -0
      packages/service-core/test/routes/api/ringtones.test.ts
  34. +14
    -0
      packages/service-core/tsconfig.json
  35. +20
    -0
      packages/service-core/tsconfig.test.json
  36. +5119
    -0
      packages/service-core/yarn.lock

+ 5
- 0
REQUIREMENTS.md Ver arquivo

@@ -1,11 +1,16 @@
# Requirements

## Authentication

- As a client, I want to log in to the service.
- [ ] In the front-end, the client provides a username, and a password to request login to the back-end.
- [ ] In the back-end, the server processes the login request from the front-end.
- As a client, I want to log out from the service.
- [ ] In the front-end, the client requests logout to the backend.
- [ ] In the back-end, the server processes the logout request from the front-end.

## Ringtone

- As a client, I want to download a ringtone.
- [ ] In the front-end, the client provides a ringtone ID to request a ringtone from the back-end.
- [ ] In the back-end, the server sends the ringtone data to the front-end.


+ 0
- 11
packages/app-web/src/models/Ringtone.ts Ver arquivo

@@ -1,11 +0,0 @@
export default class Ringtone {
name: string

data: string

createdAt: Date

updatedAt: Date

deletedAt?: Date
}

+ 4
- 0
packages/library-common/.gitignore Ver arquivo

@@ -0,0 +1,4 @@
*.log
.DS_Store
node_modules
dist

+ 21
- 0
packages/library-common/LICENSE Ver arquivo

@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 TheoryOfNekomata

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 1
- 0
packages/library-common/README.md Ver arquivo

@@ -0,0 +1 @@
# Tonality Common Library

+ 54
- 0
packages/library-common/package.json Ver arquivo

@@ -0,0 +1,54 @@
{
"version": "0.1.0",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"lint": "tsdx lint",
"prepare": "tsdx build",
"size": "size-limit",
"analyze": "size-limit --why"
},
"peerDependencies": {},
"husky": {
"hooks": {
"pre-commit": "tsdx lint"
}
},
"prettier": {
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
},
"name": "@tonality/library-common",
"author": "TheoryOfNekomata",
"module": "dist/library-common.esm.js",
"size-limit": [
{
"path": "dist/library-common.cjs.production.min.js",
"limit": "10 KB"
},
{
"path": "dist/library-common.esm.js",
"limit": "10 KB"
}
],
"devDependencies": {
"@size-limit/preset-small-lib": "^4.10.2",
"husky": "^6.0.0",
"size-limit": "^4.10.2",
"tsdx": "^0.14.1",
"tslib": "^2.2.0",
"typescript": "^4.2.4"
}
}

+ 25
- 0
packages/library-common/src/index.ts Ver arquivo

@@ -0,0 +1,25 @@
export namespace models {
export class Composer {
id?: string

name: string

bio?: string
}

export class Ringtone {
id?: string

name: string

data: string

createdAt: Date

updatedAt: Date

deletedAt?: Date

composerId: string
}
}

+ 35
- 0
packages/library-common/tsconfig.json Ver arquivo

@@ -0,0 +1,35 @@
{
// see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
"include": ["src", "types"],
"compilerOptions": {
"module": "esnext",
"lib": ["dom", "esnext"],
"importHelpers": true,
// output .d.ts declaration files for consumers
"declaration": true,
// output .js.map sourcemap files for consumers
"sourceMap": true,
// match output dir to input dir. e.g. dist/index instead of dist/src/index
"rootDir": "./src",
// stricter type-checking for stronger correctness. Recommended by TS
"strict": false,
// linter checks for common issues
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
"noUnusedLocals": false,
"noUnusedParameters": true,
// use Node's module resolution algorithm, instead of the legacy TS one
"moduleResolution": "node",
// transpile JSX to React.createElement
"jsx": "react",
// interop between ESM and CJS modules. Recommended by TS
"esModuleInterop": true,
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
"skipLibCheck": true,
// error out if import and file system have a casing mismatch. Recommended by TS
"forceConsistentCasingInFileNames": true,
// `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
"noEmit": true,
}
}

+ 8683
- 0
packages/library-common/yarn.lock
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 67
- 0
packages/service-core/.gitignore Ver arquivo

@@ -0,0 +1,67 @@
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history

# 0x
profile-*

# mac files
.DS_Store

# vim swap files
*.swp

# webstorm
.idea

# vscode
.vscode
*code-workspace

# clinic
profile*
*clinic*
*flamegraph*

# generated code
examples/typescript-server.js
test/types/index.js

# compiled app
dist
.env
.database/

+ 1
- 0
packages/service-core/README.md Ver arquivo

@@ -0,0 +1 @@
# Tonality Core Service

+ 1
- 0
packages/service-core/global.d.ts Ver arquivo

@@ -0,0 +1 @@
/// <reference types="jest" />

+ 14
- 0
packages/service-core/jest.config.js Ver arquivo

@@ -0,0 +1,14 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.test.json',
},
},
collectCoverageFrom: [
'src/modules/**/*.{js,ts}',
'src/utils/**/*.{js,ts}',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts']
};

+ 0
- 0
packages/service-core/jest.setup.ts Ver arquivo


+ 45
- 0
packages/service-core/package.json Ver arquivo

@@ -0,0 +1,45 @@
{
"name": "@tonality/service-core",
"version": "1.0.0",
"description": "",
"main": "app.ts",
"directories": {
"test": "test"
},
"scripts": {
"test": "jest",
"start": "fastify start -l info dist/app.js",
"build:ts": "tsc",
"dev": "tsc && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"tsc -w\" \"fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js\""
},
"keywords": [],
"author": "TheoryOfNekomata",
"license": "MIT",
"dependencies": {
"@abraham/reflection": "^0.8.0",
"dotenv": "^9.0.2",
"fastify": "^3.0.0",
"fastify-autoload": "^3.3.1",
"fastify-cli": "^2.11.0",
"fastify-plugin": "^3.0.0",
"fastify-sensible": "^3.1.0",
"knex": "^0.95.5",
"tsyringe": "^4.5.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/jest": "^26.0.23",
"@types/node": "^15.0.0",
"@types/uuid": "^8.3.0",
"concurrently": "^6.0.0",
"cross-env": "^7.0.3",
"fastify-tsconfig": "^1.0.1",
"jest": "^26.6.3",
"ts-jest": "^26.5.6",
"typescript": "^4.1.3"
},
"optionalDependencies": {
"@types/sqlite3": "^3.1.7",
"sqlite3": "^5.0.2"
}
}

+ 40
- 0
packages/service-core/src/app.ts Ver arquivo

@@ -0,0 +1,40 @@
import '@abraham/reflection'
import { config } from 'dotenv'

import {join} from 'path'
import AutoLoad, {AutoloadPluginOptions} from 'fastify-autoload'
import {FastifyPluginAsync} from 'fastify'

export type AppOptions = {
// Place your custom options for app below here.
} & Partial<AutoloadPluginOptions>;

const app: FastifyPluginAsync<AppOptions> = async (
fastify,
opts,
): Promise<void> => {
// Place here your custom code!

// Do not touch the following lines

// This loads all plugins defined in plugins
// those should be support plugins that are reused
// through your application
void fastify.register(AutoLoad, {
dir: join(__dirname, 'plugins'),
options: opts,
});

// This loads all plugins defined in routes
// define your routes in one of these
void fastify.register(AutoLoad, {
dir: join(__dirname, 'routes'),
options: opts,
});

};

config()

export default app;
export {app};

+ 10
- 0
packages/service-core/src/modules/composer/service.ts Ver arquivo

@@ -0,0 +1,10 @@
import { models } from '@tonality/library-common'

export default interface ComposerService {
get(id: string): Promise<models.Composer>
browse(skip?: number, take?: number): Promise<models.Composer[]>
search(q?: string): Promise<models.Composer[]>
create(data: Partial<models.Composer>): Promise<models.Composer>
update(data: Partial<models.Composer>): Promise<models.Composer>
delete(id: string): Promise<void>
}

+ 143
- 0
packages/service-core/src/modules/ringtone/controller.ts Ver arquivo

@@ -0,0 +1,143 @@
import {inject, singleton} from 'tsyringe'
import {RingtoneService} from './service'
import {DoubleDeletionError} from './response'

@singleton()
export class RingtoneController {
constructor(
@inject('RingtoneService')
private readonly ringtoneService: RingtoneService
) {
}

get = async (request: any, reply: any) => {
try {
const data = await this.ringtoneService.get(request.params['id'])
if (typeof (data.deletedAt as Date) !== 'undefined') {
reply.gone()
return
}
if (!data) {
reply.notFound()
return
}
return {
data,
}
} catch (err) {
reply.internalServerError(err.message)
}
}

browse = async (request: any, reply: any) => {
try {
// TODO deserialize query
const { 'skip': skipRaw, 'take': takeRaw } = request.query
const skipNumber = Number(skipRaw)
const takeNumber = Number(takeRaw)

const skip = !isNaN(skipNumber) ? skipNumber : undefined
const take = !isNaN(takeNumber) ? takeNumber : undefined

return {
data: await this.ringtoneService.browse(skip, take),
skip,
take,
}
} catch (err) {
reply.internalServerError(err.message)
}
}

search = async (request: any, reply: any) => {
try {
const { 'q': query } = request.query
return {
data: await this.ringtoneService.search(query),
query,
}
} catch (err) {
reply.internalServerError(err.message)
}
}

create = async (request: any, reply: any) => {
try {
const data = await this.ringtoneService.create(request.body)
reply.status(201)
return {
data,
}
} catch (err) {
reply.internalServerError(err.message)
}
}

update = async (request: any, reply: any) => {
try {
// TODO validate data
const data = await this.ringtoneService.update({
...request.body,
id: request.params['id'],
})
if (data.deletedAt !== null) {
reply.gone()
return
}
if (!data) {
reply.notFound()
return
}
return {
data,
}
} catch (err) {
reply.internalServerError(err.message)
}
}

softDelete = async (request: any, reply: any) => {
try {
// TODO validate data
const data = await this.ringtoneService.softDelete(request.params['id'])
if (!data) {
reply.notFound()
return
}
return {
data,
}
} catch (err) {
reply.internalServerError(err.message)
}
}

undoDelete = async (request: any, reply: any) => {
try {
// TODO validate data
const data = await this.ringtoneService.undoDelete(request.params['id'])
if (!data) {
reply.notFound()
return
}
return {
data,
}
} catch (err) {
reply.internalServerError(err.message)
}
}

hardDelete = async (request: any, reply: any) => {
try {
// TODO validate data
await this.ringtoneService.hardDelete(request.params['id'])
reply.status(204)
} catch (err) {
if (err instanceof DoubleDeletionError) {
reply.notFound(err.message)
}
reply.internalServerError(err.message)
}
}
}

+ 17
- 0
packages/service-core/src/modules/ringtone/index.ts Ver arquivo

@@ -0,0 +1,17 @@
import { container } from 'tsyringe'
import { RingtoneController } from './controller'
import { RingtoneService } from './service'
import { RingtoneRepository } from './repository'

const ringtoneModule = container.createChildContainer()

ringtoneModule.register('RingtoneController', { useClass: RingtoneController })
ringtoneModule.register('RingtoneService', { useClass: RingtoneService })
ringtoneModule.register('RingtoneRepository', { useClass: RingtoneRepository })
ringtoneModule.register('DATABASE_TYPE', { useValue: process.env.DATABASE_TYPE })
ringtoneModule.register('DATABASE_LOCATION', { useValue: process.env.DATABASE_LOCATION })
ringtoneModule.register('DATABASE_USERNAME', { useValue: process.env.DATABASE_USERNAME })
ringtoneModule.register('DATABASE_PASSWORD', { useValue: process.env.DATABASE_PASSWORD })
ringtoneModule.register('DATABASE_SCHEMA', { useValue: process.env.DATABASE_SCHEMA })

export default ringtoneModule

+ 261
- 0
packages/service-core/src/modules/ringtone/repository.ts Ver arquivo

@@ -0,0 +1,261 @@
import {inject, injectable} from 'tsyringe'
import * as sqlite from 'sqlite3'
import {models} from '@tonality/library-common'

@injectable()
export class RingtoneRepository {
private readonly database: sqlite.Database;
constructor(
@inject('DATABASE_LOCATION')
DATABASE_LOCATION: string
) {
this.database = new sqlite.Database(DATABASE_LOCATION)
}

async get(id: string): Promise<models.Ringtone> {
return new Promise((resolve, reject) => {
this.database.get(
`
SELECT *
FROM ringtone
WHERE id = ?;
`,
[id],
(err, result) => {
if (err) {
reject(err)
return
}
resolve(result)
}
)
})
}

async browse(skip: number, take: number): Promise<models.Ringtone[]> {
return new Promise((resolve, reject) => {
this.database.all(
`
SELECT *
FROM ringtone
LIMIT ?
OFFSET ?;
`,
[take, skip],
(err, result) => {
if (err) {
reject(err)
return
}

resolve(result)
})
})
}

async search(q: string): Promise<models.Ringtone[]> {
return new Promise((resolve, reject) => {
this.database.all(
`
SELECT *
FROM ringtone r
JOIN composer c ON r.composerId = c.id
WHERE r.name LIKE ? ESCAPE '\';
`,
[`%${q.replace('\\', '\\\\')}%`],
(err, result) => {
if (err) {
reject(err)
return
}
resolve(result)
}
)
})
}

async create(data: Partial<models.Ringtone>): Promise<models.Ringtone> {
return new Promise((resolve, reject) => {

this.database.serialize(() => {
this.database.run(
`
INSERT
INTO ringtone
VALUES (?, ?, ?, ?, ?, NULL, ?);
`,
[data.id, data.name, data.data, data.createdAt, data.updatedAt, data.composerId],
(err) => {
if (err) {
reject(err)
return
}
}
)
this.database.get(
`
SELECT *
FROM ringtone
WHERE id = ?;
`,
[data.id],
(err, result) => {
if (err) {
reject(err)
return
}
resolve(result)
}
)
})
})
}

async update(data: Partial<models.Ringtone>): Promise<models.Ringtone> {
return new Promise((resolve, reject) => {
this.database.serialize(() => {
this.database.run(
`
UPDATE ringtone
SET
name = ?,
data = ?,
updatedAt = ?
WHERE id = ?;
`,
[data.name, data.data, data.updatedAt, data.id],
(err) => {
if (err) {
reject(err)
return
}
}
)
this.database.get(
`
SELECT *
FROM ringtone
WHERE id = ?;
`,
[data.id],
(err, result) => {
if (err) {
reject(err)
return
}
console.log(result, data.id)
resolve(result)
}
)
})
})
}

async softDelete(id: string, deletedAt: Date): Promise<models.Ringtone> {
return new Promise((resolve, reject) => {
this.database.serialize(() => {
this.database.run(
`
UPDATE ringtone
SET deletedAt = ?
WHERE id = ?
`,
[deletedAt, id],
(err) => {
if (err) {
reject(err)
return
}
}
)
this.database.get(
`
SELECT *
FROM ringtone
WHERE id = ?;
`,
[id],
(err, result) => {
if (err) {
reject(err)
return
}
resolve(result)
}
)
})
})
}

async undoDelete(id: string): Promise<models.Ringtone> {
return new Promise((resolve, reject) => {
this.database.serialize(() => {
this.database.run(
`
UPDATE ringtone
SET deletedAt = NULL
WHERE id = ?
`,
[id],
(err) => {
if (err) {
reject(err)
return
}
}
)
this.database.get(
`
SELECT *
FROM ringtone
WHERE id = ?;
`,
[id],
(err, result) => {
if (err) {
reject(err)
return
}
resolve(result)
}
)
})
})
}

async hardDelete(id: string): Promise<models.Ringtone> {
return new Promise((resolve, reject) => {
this.database.serialize(() => {
this.database.get(
`
SELECT *
FROM ringtone
WHERE id = ?;
`,
[id],
(err, result) => {
if (err) {
reject(err)
return
}
resolve(result)
}
)
this.database.run(
`
DELETE
FROM ringtone
WHERE id = ?
`,
[id],
(err) => {
if (err) {
reject(err)
return
}
}
)
})
})
}
}

+ 1
- 0
packages/service-core/src/modules/ringtone/response.ts Ver arquivo

@@ -0,0 +1 @@
export class DoubleDeletionError extends Error {}

+ 7
- 0
packages/service-core/src/modules/ringtone/service.test.ts Ver arquivo

@@ -0,0 +1,7 @@
describe('ringtone service', () => {
describe('retrieval of single ringtone', () => {
it('should get from the repository', () => {

})
})
})

+ 61
- 0
packages/service-core/src/modules/ringtone/service.ts Ver arquivo

@@ -0,0 +1,61 @@
import {inject, singleton} from 'tsyringe'
import {v4} from 'uuid'
import { models } from '@tonality/library-common'
import {RingtoneRepository} from './repository'
import {DoubleDeletionError} from './response'

@singleton()
export class RingtoneService {
constructor(
@inject('RingtoneRepository')
private readonly ringtoneRepository: RingtoneRepository
) {
}

async get(id: string): Promise<models.Ringtone> {
return this.ringtoneRepository.get(id)
}

async browse(skip: number = 0, take: number = 10): Promise<models.Ringtone[]> {
return this.ringtoneRepository.browse(skip, take)
}

async search(q: string = ''): Promise<models.Ringtone[]> {
return this.ringtoneRepository.search(q)
}

async create(data: Partial<models.Ringtone>): Promise<models.Ringtone> {
const { createdAt, updatedAt, deletedAt, ...safeData } = data
const now = new Date()
const newId = v4()
return this.ringtoneRepository.create({
...safeData,
id: newId,
createdAt: now,
updatedAt: now,
})
}

async update(data: Partial<models.Ringtone>): Promise<models.Ringtone> {
const { createdAt, updatedAt, deletedAt, ...safeData } = data
return this.ringtoneRepository.update({
...safeData,
updatedAt: new Date(),
})
}

async softDelete(id: string): Promise<models.Ringtone> {
return this.ringtoneRepository.softDelete(id, new Date())
}

async undoDelete(id: string): Promise<models.Ringtone> {
return this.ringtoneRepository.undoDelete(id)
}

async hardDelete(id: string): Promise<void> {
const deleted = await this.ringtoneRepository.hardDelete(id)
if (!deleted) {
throw new DoubleDeletionError('Attempt to delete non-existing ringtone.')
}
}
}

+ 13
- 0
packages/service-core/src/plugins/sensible.ts Ver arquivo

@@ -0,0 +1,13 @@
import fp from 'fastify-plugin';
import sensible, {SensibleOptions} from 'fastify-sensible';

/**
* This plugins adds some utilities to handle http errors
*
* @see https://github.com/fastify/fastify-sensible
*/
export default fp<SensibleOptions>(async (fastify, opts) => {
fastify.register(sensible, {
errorHandler: false,
});
});

+ 16
- 0
packages/service-core/src/routes/api/ringtones/index.ts Ver arquivo

@@ -0,0 +1,16 @@
import {FastifyPluginAsync} from 'fastify'
import ringtoneModule from '../../../modules/ringtone'
import {RingtoneController} from '../../../modules/ringtone/controller'

const ringtones: FastifyPluginAsync = async (fastify): Promise<void> => {
const ringtoneController = ringtoneModule.resolve<RingtoneController>('RingtoneController')
fastify.get('/', ringtoneController.browse)
fastify.get('/:id', ringtoneController.get)
fastify.post('/', ringtoneController.create)
fastify.patch('/:id', ringtoneController.update)
fastify.post('/:id/delete', ringtoneController.softDelete)
fastify.delete('/:id/delete', ringtoneController.undoDelete)
fastify.delete('/:id', ringtoneController.hardDelete)
}

export default ringtones

+ 10
- 0
packages/service-core/src/routes/api/search/ringtones/index.ts Ver arquivo

@@ -0,0 +1,10 @@
import {FastifyPluginAsync} from 'fastify'
import ringtoneModule from '../../../../modules/ringtone'
import {RingtoneController} from '../../../../modules/ringtone/controller'

const ringtones: FastifyPluginAsync = async (fastify): Promise<void> => {
const ringtoneController = ringtoneModule.resolve<RingtoneController>('RingtoneController')
fastify.get('/', ringtoneController.search)
}

export default ringtones

+ 9
- 0
packages/service-core/src/routes/example/index.ts Ver arquivo

@@ -0,0 +1,9 @@
import {FastifyPluginAsync} from "fastify";

const example: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get('/', async function (request, reply) {
return 'this is an example';
});
};

export default example;

+ 9
- 0
packages/service-core/src/routes/root.ts Ver arquivo

@@ -0,0 +1,9 @@
import {FastifyPluginAsync} from 'fastify'

const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get('/', async function (request, reply) {
return {root: true};
})
}

export default root

+ 111
- 0
packages/service-core/src/utils/database/index.ts Ver arquivo

@@ -0,0 +1,111 @@
import {Knex, knex} from 'knex'

export type QueryBuilder = Knex

export type DatabaseOptions = {
engine: unknown,
type: string,
location: string,
username?: string,
password?: string,
schema?: string,
version?: string,
}

const normalTypes: Record<string, string> = {
postgres: 'pg',
pg: 'pg',
postgresql: 'pg',
mysql: 'mysql',
mariadb: 'mysql',
sqlite: 'sqlite3',
sqlite3: 'sqlite3',
}

export const createQueryBuilder = ({
type,
engine,
location,
...etcOptions
}: DatabaseOptions) => {
const { [type]: theType = null } = normalTypes

if (theType === null) {
throw new RangeError(`Invalid type: ${type}`)
}

switch (theType as string) {
case 'sqlite3':
return knex({
client: theType,
connection: {
filename: location,
}
})
default:
break
}

const { username, password, schema, version, } = etcOptions
return knex({
client: theType,
version,
connection: {
host: location,
user: username,
password,
database: schema,
},
})
}

export class Database {
private readonly queryBuilder: QueryBuilder

constructor({
type,
engine,
location,
...etcOptions
}: DatabaseOptions) {
const { [type]: theType = null } = normalTypes

if (theType === null) {
throw new RangeError(`Invalid type: ${type}`)
}

switch (theType as string) {
case 'sqlite3':
this.queryBuilder = knex({
client: theType,
connection: {
filename: location,
}
})
return
default:
break
}

const { username, password, schema, version, } = etcOptions
this.queryBuilder = knex({
client: theType,
version,
connection: {
host: location,
user: username,
password,
database: schema,
},
})
}

async run(cb: (qb: QueryBuilder) => any) {
const rawQuery = cb(this.queryBuilder)
if (!rawQuery) {
throw new Error('Invalid callback.')
}

return rawQuery
}
}

+ 4
- 0
packages/service-core/src/utils/helpers/index.ts Ver arquivo

@@ -0,0 +1,4 @@
export interface Paginated<T> extends Array<T> {
take: number,
skip: number,
}

+ 22
- 0
packages/service-core/test/helper.ts Ver arquivo

@@ -0,0 +1,22 @@
// This file contains code that we reuse between our tests.
import Fastify from 'fastify'
import fp from 'fastify-plugin'
import App from '../src/app'

// Fill in this config with all the configurations
// needed for testing the application
export const config = async () => {
return {};
}

// Automatically build and tear down our instance
export const build = async () => {
const app = Fastify()
// fastify-plugin ensures that all decorators
// are exposed for testing purposes, this is
// different from the production setup
void app.register(fp(App), await config())
await app.ready()
return app
}


+ 83
- 0
packages/service-core/test/mocks/repositories/Ringtone.ts Ver arquivo

@@ -0,0 +1,83 @@
import {models} from '@tonality/library-common'

export default class MockRingtoneRepository {
constructor(private items: models.Ringtone[]) {}

async get(id: string) {
return this.items.find(item => item.id === id)
}

async browse(skip: number, take: number) {
return this.items.slice(skip, skip + take)
}

async search(q: string) {
return this.items.filter(item => item.name.toLowerCase().includes(q.toLowerCase()))
}

async create(data: Partial<models.Ringtone>) {
let newItem: models.Ringtone
this.items.push(newItem = {
id: data.id,
name: data.name,
data: data.data,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
deletedAt: null,
composerId: data.composerId,
})
return newItem
}

async update(data: Partial<models.Ringtone>) {
let updated: models.Ringtone
this.items = this.items.map(item => (
item.id === data.id
? (updated = {
...item,
...data,
id: item.id,
})
: item
))
return updated
}

async softDelete(id: string, deletedAt: Date) {
let deleted: models.Ringtone
this.items = this.items.map(item => (
item.id === id
? (deleted = {
...item,
deletedAt,
})
: item
))
return deleted
}

async undoDelete(id: string) {
let undo: models.Ringtone
this.items = this.items.map(item => (
item.id === id
? (undo = {
...item,
deletedAt: null,
})
: item
))
return undo
}

async hardDelete(id: string) {
let deleted: models.Ringtone
this.items = this.items.filter(item => {
if (item.id === id) {
deleted = item
return false
}
return true
})
return deleted
}
}

+ 87
- 0
packages/service-core/test/routes/api/ringtones.test.ts Ver arquivo

@@ -0,0 +1,87 @@
import {FastifyInstance} from 'fastify'
import {build} from '../../helper'
import ringtoneModule from '../../../src/modules/ringtone'
import MockRingtoneRepository from '../../mocks/repositories/Ringtone'

describe('ringtone resource', () => {
let app: FastifyInstance

beforeEach(async () => {
ringtoneModule.registerInstance('RingtoneRepository', new MockRingtoneRepository([
{
id: '00000000-0000-0000-000000000000',
name: 'Ringtone 1',
data: '4c4',
createdAt: new Date('2021-05-01'),
updatedAt: new Date('2021-05-01'),
deletedAt: null,
composerId: '00000000-0000-0000-000000000000',
},
{
id: '00000000-0000-0000-000000000001',
name: 'Ringtone 2',
data: '4c4 8c4',
createdAt: new Date('2021-05-01'),
updatedAt: new Date('2021-05-01'),
deletedAt: null,
composerId: '00000000-0000-0000-000000000000',
},
{
id: '00000000-0000-0000-000000000002',
name: 'Deleted Ringtone 1',
data: '4c4',
createdAt: new Date('2021-05-01'),
updatedAt: new Date('2021-05-01'),
deletedAt: new Date('2021-05-05'),
composerId: '00000000-0000-0000-000000000000',
}
]))
app = await build()
})

afterEach(async () => {
ringtoneModule.clearInstances()
await app.close()
})

describe('collection', () => {
it('should be browsable', async () => {
const res = await app.inject({
url: '/api/ringtones',
method: 'GET',
})
const parsedPayload = JSON.parse(res.payload)
expect(Array.isArray(parsedPayload.data)).toBe(true)
})

it('should be searchable', async () => {
// TODO
})

it('should be extendable', async () => {
const res = await app.inject({
url: '/api/ringtones',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
payload: JSON.stringify({
name: 'New Ringtone',
data: '4c4',
composerId: '00000000-0000-0000-000000000000',
})
})
const parsedPayload = JSON.parse(res.payload)
expect(parsedPayload.data).toEqual({
id: expect.any(String),
name: 'New Ringtone',
data: '4c4',
createdAt: expect.any(String),
updatedAt: expect.any(String),
deletedAt: null,
composerId: '00000000-0000-0000-000000000000',
})
console.log(parsedPayload)
})
})
})

+ 14
- 0
packages/service-core/tsconfig.json Ver arquivo

@@ -0,0 +1,14 @@
{
"extends": "fastify-tsconfig",
"compilerOptions": {
"outDir": "dist",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": [
"src/**/*.ts"
],
"exclude": [
"**/*.test.ts"
]
}

+ 20
- 0
packages/service-core/tsconfig.test.json Ver arquivo

@@ -0,0 +1,20 @@
{
"extends": "fastify-tsconfig",
"compilerOptions": {
"strict": false,
"noEmit": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"types": [
"node",
"jest"
]
},
"include": [
"test/routes/**/*.ts",
"src/**/*.test.ts"
],
"exclude": [
"dist/**/*"
]
}

+ 5119
- 0
packages/service-core/yarn.lock
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


Carregando…
Cancelar
Salvar