소스 검색

Set up code for backend

Scaffold backend with Fastify CLI and replace Tap with Jest for
consistency.
master
부모
커밋
3d7dbd4439
36개의 변경된 파일15013개의 추가작업 그리고 11개의 파일을 삭제
  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 파일 보기

@@ -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 파일 보기

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

data: string

createdAt: Date

updatedAt: Date

deletedAt?: Date
}

+ 4
- 0
packages/library-common/.gitignore 파일 보기

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

+ 21
- 0
packages/library-common/LICENSE 파일 보기

@@ -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 파일 보기

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

+ 54
- 0
packages/library-common/package.json 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


+ 67
- 0
packages/service-core/.gitignore 파일 보기

@@ -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 파일 보기

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

+ 1
- 0
packages/service-core/global.d.ts 파일 보기

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

+ 14
- 0
packages/service-core/jest.config.js 파일 보기

@@ -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 파일 보기


+ 45
- 0
packages/service-core/package.json 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

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

+ 7
- 0
packages/service-core/src/modules/ringtone/service.test.ts 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

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

+ 22
- 0
packages/service-core/test/helper.ts 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


불러오는 중...
취소
저장