Selaa lähdekoodia

Isolate libraries

Separate reusable code from service-core.
master
TheoryOfNekomata 3 vuotta sitten
vanhempi
commit
8d2d37d1f3
34 muutettua tiedostoa jossa 9484 lisäystä ja 599 poistoa
  1. +17
    -7
      packages/library-common/src/index.ts
  2. +4
    -0
      packages/library-uuid/.gitignore
  3. +21
    -0
      packages/library-uuid/LICENSE
  4. +103
    -0
      packages/library-uuid/README.md
  5. +59
    -0
      packages/library-uuid/package.json
  6. +18
    -0
      packages/library-uuid/src/index.ts
  7. +7
    -0
      packages/library-uuid/test/blah.test.ts
  8. +35
    -0
      packages/library-uuid/tsconfig.json
  9. +8484
    -0
      packages/library-uuid/yarn.lock
  10. +3
    -3
      packages/service-core/.env.example
  11. +1
    -0
      packages/service-core/jest.setup.ts
  12. +4
    -0
      packages/service-core/package.json
  13. +41
    -0
      packages/service-core/prisma/schema.prisma
  14. +9
    -0
      packages/service-core/src/app.ts
  15. +7
    -0
      packages/service-core/src/global.ts
  16. +0
    -10
      packages/service-core/src/modules/composer/service.ts
  17. +6
    -0
      packages/service-core/src/modules/password/index.ts
  18. +24
    -0
      packages/service-core/src/modules/password/service.ts
  19. +38
    -15
      packages/service-core/src/modules/ringtone/controller.ts
  20. +7
    -16
      packages/service-core/src/modules/ringtone/index.ts
  21. +0
    -261
      packages/service-core/src/modules/ringtone/repository.ts
  22. +38
    -2
      packages/service-core/src/modules/ringtone/service.test.ts
  23. +81
    -33
      packages/service-core/src/modules/ringtone/service.ts
  24. +6
    -0
      packages/service-core/src/modules/user/index.ts
  25. +147
    -0
      packages/service-core/src/modules/user/service.ts
  26. +3
    -3
      packages/service-core/src/routes/api/ringtones/index.ts
  27. +3
    -3
      packages/service-core/src/routes/api/search/ringtones/index.ts
  28. +0
    -111
      packages/service-core/src/utils/database/index.ts
  29. +12
    -2
      packages/service-core/src/utils/helpers/index.ts
  30. +42
    -0
      packages/service-core/src/utils/mocks/index.ts
  31. +0
    -83
      packages/service-core/test/mocks/repositories/Ringtone.ts
  32. +63
    -43
      packages/service-core/test/routes/api/ringtones.test.ts
  33. +69
    -0
      packages/service-core/test/routes/api/search/ringtones.test.ts
  34. +132
    -7
      packages/service-core/yarn.lock

+ 17
- 7
packages/library-common/src/index.ts Näytä tiedosto

@@ -1,14 +1,16 @@
import Uuid from '@tonality/library-uuid'

export namespace models {
export class Composer {
id?: string
export class User {
id: Uuid

name: string
username: string

bio?: string
password: string
}

export class Ringtone {
id?: string
id: Uuid

name: string

@@ -20,8 +22,16 @@ export namespace models {

updatedAt: Date

deletedAt?: Date
deletedAt?: Date | null

composerUserId: Uuid
}

export class UserProfile {
userId: Uuid

bio?: string | null

composerId: string
email?: string | null
}
}

+ 4
- 0
packages/library-uuid/.gitignore Näytä tiedosto

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

+ 21
- 0
packages/library-uuid/LICENSE Näytä tiedosto

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

Copyright (c) 2021 TheoryOfNekomata <allan.crisostomo@outlook.com>

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.

+ 103
- 0
packages/library-uuid/README.md Näytä tiedosto

@@ -0,0 +1,103 @@
# TSDX User Guide

Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it.

> This TSDX setup is meant for developing libraries (not apps!) that can be published to NPM. If you’re looking to build a Node app, you could use `ts-node-dev`, plain `ts-node`, or simple `tsc`.

> If you’re new to TypeScript, checkout [this handy cheatsheet](https://devhints.io/typescript)

## Commands

TSDX scaffolds your new library inside `/src`.

To run TSDX, use:

```bash
npm start # or yarn start
```

This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`.

To do a one-off build, use `npm run build` or `yarn build`.

To run tests, use `npm test` or `yarn test`.

## Configuration

Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly.

### Jest

Jest tests are set up to run with `npm test` or `yarn test`.

### Bundle Analysis

[`size-limit`](https://github.com/ai/size-limit) is set up to calculate the real cost of your library with `npm run size` and visualize the bundle with `npm run analyze`.

#### Setup Files

This is the folder structure we set up for you:

```txt
/src
index.tsx # EDIT THIS
/test
blah.test.tsx # EDIT THIS
.gitignore
package.json
README.md # EDIT THIS
tsconfig.json
```

### Rollup

TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details.

### TypeScript

`tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs.

## Continuous Integration

### GitHub Actions

Two actions are added by default:

- `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix
- `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit)

## Optimizations

Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations:

```js
// ./types/index.d.ts
declare var __DEV__: boolean;

// inside your code...
if (__DEV__) {
console.log('foo');
}
```

You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions.

## Module Formats

CJS, ESModules, and UMD module formats are supported.

The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found.

## Named Exports

Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library.

## Including Styles

There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like.

For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader.

## Publishing to NPM

We recommend using [np](https://github.com/sindresorhus/np).

+ 59
- 0
packages/library-uuid/package.json Näytä tiedosto

@@ -0,0 +1,59 @@
{
"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",
"test": "tsdx test",
"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-uuid",
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"module": "dist/library-uuid.esm.js",
"size-limit": [
{
"path": "dist/library-uuid.cjs.production.min.js",
"limit": "10 KB"
},
{
"path": "dist/library-uuid.esm.js",
"limit": "10 KB"
}
],
"devDependencies": {
"@size-limit/preset-small-lib": "^4.11.0",
"@types/uuid": "^8.3.0",
"husky": "^6.0.0",
"size-limit": "^4.11.0",
"tsdx": "^0.14.1",
"tslib": "^2.2.0",
"typescript": "^4.3.2"
},
"dependencies": {
"uuid": "^8.3.2"
}
}

+ 18
- 0
packages/library-uuid/src/index.ts Näytä tiedosto

@@ -0,0 +1,18 @@
import {parse, stringify, v4} from 'uuid';

export default class Uuid extends Buffer {
static new() {
const raw = v4()
const bytes = parse(raw) as Uint8Array
return Uuid.from(bytes) as Uuid
}

toString() {
return stringify(this)
}

static parse(s: string) {
const bytes = parse(s) as Uint8Array
return Uuid.from(bytes) as Uuid
}
}

+ 7
- 0
packages/library-uuid/test/blah.test.ts Näytä tiedosto

@@ -0,0 +1,7 @@
import { sum } from '../src';

describe('blah', () => {
it('works', () => {
expect(sum(1, 1)).toEqual(2);
});
});

+ 35
- 0
packages/library-uuid/tsconfig.json Näytä tiedosto

@@ -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": true,
// linter checks for common issues
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
"noUnusedLocals": true,
"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,
}
}

+ 8484
- 0
packages/library-uuid/yarn.lock
File diff suppressed because it is too large
Näytä tiedosto


+ 3
- 3
packages/service-core/.env.example Näytä tiedosto

@@ -1,8 +1,6 @@
PORT=

DATABASE_TYPE=

DATABASE_LOCATION=
DATABASE_URL=

DATABASE_USERNAME=

@@ -11,3 +9,5 @@ DATABASE_PASSWORD=
DATABASE_SCHEMA=

APP_BASE_URL=

PASSWORD_GEN_SALT_ROUNDS=

+ 1
- 0
packages/service-core/jest.setup.ts Näytä tiedosto

@@ -1,3 +1,4 @@
import '@abraham/reflection'
import { config } from 'dotenv'

config()

+ 4
- 0
packages/service-core/package.json Näytä tiedosto

@@ -17,6 +17,8 @@
"license": "MIT",
"dependencies": {
"@abraham/reflection": "^0.8.0",
"@prisma/client": "^2.24.1",
"bcrypt": "^5.0.1",
"dotenv": "^9.0.2",
"fastify": "^3.0.0",
"fastify-autoload": "^3.3.1",
@@ -29,6 +31,7 @@
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/jest": "^26.0.23",
"@types/node": "^15.0.0",
"@types/uuid": "^8.3.0",
@@ -36,6 +39,7 @@
"cross-env": "^7.0.3",
"fastify-tsconfig": "^1.0.1",
"jest": "^26.6.3",
"prisma": "^2.24.1",
"ts-jest": "^26.5.6",
"typescript": "^4.1.3"
},


+ 41
- 0
packages/service-core/prisma/schema.prisma Näytä tiedosto

@@ -0,0 +1,41 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}

model Ringtone {
id Bytes @id
name String
composerUserId Bytes @map("composer_user_id")
tempo Int @default(120)
data String @default("")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @map("updated_at")
deletedAt DateTime? @map("deleted_at")
user User @relation(fields: [composerUserId], references: [id])

@@map("ringtone")
}

model User {
id Bytes @id
username String @unique
password String
ringtone Ringtone[]
userProfile UserProfile?

@@map("user")
}

model UserProfile {
userId Bytes @id @map("user_id")
bio String? @default("")
email String? @default("")
user User @relation(fields: [userId], references: [id])

@@map("user_profile")
}

+ 9
- 0
packages/service-core/src/app.ts Näytä tiedosto

@@ -3,6 +3,7 @@ import '@abraham/reflection'
import {join} from 'path'
import AutoLoad, {AutoloadPluginOptions} from 'fastify-autoload'
import {FastifyPluginAsync} from 'fastify'
import {container} from 'tsyringe';

export type AppOptions = {
// Place your custom options for app below here.
@@ -13,6 +14,14 @@ const app: FastifyPluginAsync<AppOptions> = async (
opts,
): Promise<void> => {
// Place here your custom code!
const modules = await Promise.all([
import('./global'),
import('./modules/password'),
import('./modules/user'),
import('./modules/ringtone'),
])

modules.forEach(m => m.default(container))

// Do not touch the following lines



+ 7
- 0
packages/service-core/src/global.ts Näytä tiedosto

@@ -0,0 +1,7 @@
import {DependencyContainer} from 'tsyringe';
import {PrismaClient} from '@prisma/client';

export default (container: DependencyContainer) => {
container.register('PrismaClient', { useValue: new PrismaClient() })
container.register('PASSWORD_GEN_SALT_ROUNDS', { useValue: process.env.PASSWORD_GEN_SALT_ROUNDS })
}

+ 0
- 10
packages/service-core/src/modules/composer/service.ts Näytä tiedosto

@@ -1,10 +0,0 @@
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>
}

+ 6
- 0
packages/service-core/src/modules/password/index.ts Näytä tiedosto

@@ -0,0 +1,6 @@
import {DependencyContainer} from 'tsyringe';
import {PasswordServiceImpl} from './service';

export default (container: DependencyContainer) => {
container.register('PasswordService', { useClass: PasswordServiceImpl })
}

+ 24
- 0
packages/service-core/src/modules/password/service.ts Näytä tiedosto

@@ -0,0 +1,24 @@
import { genSalt, hash, compare } from 'bcrypt';
import {inject, singleton} from 'tsyringe';

export default interface PasswordService {
hash(password: string): Promise<string>
compare(password: string, hash: string): Promise<boolean>
}

@singleton()
export class PasswordServiceImpl implements PasswordService {
constructor(
@inject('PASSWORD_GEN_SALT_ROUNDS')
private readonly passwordGenSaltRounds: number,
) {}

async hash(password: string) {
const salt = await genSalt(this.passwordGenSaltRounds)
return await hash(password, salt)
}

async compare(password: string, hash: string) {
return await compare(password, hash)
}
}

+ 38
- 15
packages/service-core/src/modules/ringtone/controller.ts Näytä tiedosto

@@ -1,18 +1,34 @@
import {inject, singleton} from 'tsyringe'
import {RingtoneService} from './service'
import {models} from '@tonality/library-common'
import Uuid from '@tonality/library-uuid'
import {Controller, ControllerResponse, PaginatedResponse} from '../../utils/helpers'
import RingtoneService from './service'
import {DoubleDeletionError} from './response'

type RingtoneController = Controller<{
get: ControllerResponse<models.Ringtone>,
browse: PaginatedResponse<models.Ringtone>,
search: ControllerResponse<models.Ringtone>,
create: ControllerResponse<models.Ringtone>,
update: ControllerResponse<models.Ringtone>,
softDelete: ControllerResponse<models.Ringtone>,
undoDelete: ControllerResponse<models.Ringtone>,
hardDelete: undefined,
}>

export default RingtoneController

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

get = async (request: any, reply: any) => {
try {
const data = await this.ringtoneService.get(request.params['id'])
const id = Uuid.parse(request.params['id'])
const data = await this.ringtoneService.get(id)
if (typeof (data.deletedAt as Date) !== 'undefined') {
reply.raw.statusMessage = 'Ringtone Deleted Previously'
reply.gone()
@@ -40,8 +56,8 @@ export class RingtoneController {
const skipNumber = Number(skipRaw)
const takeNumber = Number(takeRaw)

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

reply.raw.statusMessage = 'Multiple Ringtones Retrieved'
return {
@@ -61,7 +77,6 @@ export class RingtoneController {
reply.raw.statusMessage = 'Search Results Retrieved'
return {
data: await this.ringtoneService.search(query),
query,
}
} catch (err) {
reply.raw.statusMessage = 'Search Error'
@@ -71,7 +86,11 @@ export class RingtoneController {

create = async (request: any, reply: any) => {
try {
const data = await this.ringtoneService.create(request.body)
const { composerUserId, ...etcBody } = request.body
const data = await this.ringtoneService.create({
...etcBody,
composerUserId: Uuid.parse(composerUserId),
})
reply.raw.statusMessage = 'Ringtone Created'
reply.raw.statusCode = 201
return {
@@ -86,11 +105,12 @@ export class RingtoneController {
update = async (request: any, reply: any) => {
try {
// TODO validate data
const id = Uuid.parse(request.params['id'])
const data = await this.ringtoneService.update({
...request.body,
id: request.params['id'],
id,
})
if (data.deletedAt !== null) {
if (data.deletedAt) {
reply.raw.statusMessage = 'Ringtone Deleted Previously'
reply.gone()
return
@@ -112,7 +132,8 @@ export class RingtoneController {
softDelete = async (request: any, reply: any) => {
try {
// TODO validate data
const data = await this.ringtoneService.softDelete(request.params['id'])
const id = Uuid.parse(request.params['id'])
const data = await this.ringtoneService.softDelete(id)
if (!data) {
reply.raw.statusMessage = 'Ringtone Not Found'
reply.notFound()
@@ -131,7 +152,8 @@ export class RingtoneController {
undoDelete = async (request: any, reply: any) => {
try {
// TODO validate data
const data = await this.ringtoneService.undoDelete(request.params['id'])
const id = Uuid.parse(request.params['id'])
const data = await this.ringtoneService.undoDelete(id)
if (!data) {
reply.raw.statusMessage = 'Ringtone Not Found'
reply.notFound()
@@ -150,7 +172,8 @@ export class RingtoneController {
hardDelete = async (request: any, reply: any) => {
try {
// TODO validate data
await this.ringtoneService.hardDelete(request.params['id'])
const id = Uuid.parse(request.params['id'])
await this.ringtoneService.hardDelete(id)
reply.status(204)
reply.raw.statusMessage = 'Ringtone Hard-Deleted'
} catch (err) {


+ 7
- 16
packages/service-core/src/modules/ringtone/index.ts Näytä tiedosto

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

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
export default (container: DependencyContainer) => {
container.register('RingtoneController', { useClass: RingtoneControllerImpl })
container.register('RingtoneService', { useClass: RingtoneServiceImpl })
}

+ 0
- 261
packages/service-core/src/modules/ringtone/repository.ts Näytä tiedosto

@@ -1,261 +0,0 @@
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 LOWER(r.name) LIKE ? ESCAPE '\';
`,
[`%${q.toLowerCase().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
}
}
)
})
})
}
}

+ 38
- 2
packages/service-core/src/modules/ringtone/service.test.ts Näytä tiedosto

@@ -1,7 +1,43 @@
import {PrismaClient as RealPrismaClient} from '@prisma/client';
import RingtoneService from './service';
import {models} from '@tonality/library-common';
import {container} from 'tsyringe';
import {MockPrismaRepository} from '../../utils/mocks';
import Uuid from '@tonality/library-uuid';
import globalBootstrap from '../../global'
import bootstrap from '.'

jest.mock('@prisma/client')
const PrismaClient = RealPrismaClient as jest.Mock

describe('ringtone service', () => {
describe('retrieval of single ringtone', () => {
it('should get from the repository', () => {
let ringtoneService: RingtoneService

beforeAll(() => {
globalBootstrap(container)
})

beforeAll(() => {
bootstrap(container)
})

beforeEach(() => {
PrismaClient.mockImplementationOnce(() => ({
ringtone: new MockPrismaRepository<models.Ringtone>([], 'id'),
}))
})

beforeEach(() => {
ringtoneService = container.resolve('RingtoneService')
})

describe('retrieval of single ringtone', () => {
it('should get from the repository', async () => {
const prismaClient: RealPrismaClient = container.resolve('PrismaClient')
console.log(prismaClient)
const findUnique = jest.spyOn(prismaClient.ringtone, 'findUnique')
await ringtoneService.get(Uuid.new())
expect(findUnique).toBeCalled()
})
})
})

+ 81
- 33
packages/service-core/src/modules/ringtone/service.ts Näytä tiedosto

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

export default interface RingtoneService {
get(id: Uuid): Promise<models.Ringtone>
browse(skip?: number, take?: number): Promise<models.Ringtone[]>
search(q?: string): Promise<models.Ringtone>
create(data: Partial<models.Ringtone>): Promise<models.Ringtone>
update(data: Partial<models.Ringtone>): Promise<models.Ringtone>
softDelete(id: Uuid): Promise<models.Ringtone>
undoDelete(id: Uuid): Promise<models.Ringtone>
hardDelete(id: Uuid): Promise<void>
}

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

async get(id: Uuid): Promise<models.Ringtone> {
const ringtone = await this.prismaClient.ringtone.findUnique({
where: {
id,
}
})

if (!ringtone) {
throw new Error('Ringtone not found!')
}

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

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

async search(q: string = ''): Promise<models.Ringtone[]> {
return this.ringtoneRepository.search(q)
return this.prismaClient.ringtone.findMany({
where: {
name: {
contains: 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,
const { createdAt, updatedAt, deletedAt, composerUserId, name, ...safeData } = data
return this.prismaClient.ringtone.create({
data: {
...safeData,
id: Uuid.new(),
composerUserId: composerUserId as Uuid,
name: name as string,
},
})
}

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

async softDelete(id: string): Promise<models.Ringtone> {
return this.ringtoneRepository.softDelete(id, new Date())
async softDelete(id: Uuid): Promise<models.Ringtone> {
return this.prismaClient.ringtone.update({
where: {
id,
},
data: {
deletedAt: new Date(),
},
})
}

async undoDelete(id: string): Promise<models.Ringtone> {
return this.ringtoneRepository.undoDelete(id)
async undoDelete(id: Uuid): Promise<models.Ringtone> {
return this.prismaClient.ringtone.update({
where: {
id,
},
data: {
deletedAt: null,
},
})
}

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

+ 6
- 0
packages/service-core/src/modules/user/index.ts Näytä tiedosto

@@ -0,0 +1,6 @@
import {DependencyContainer} from 'tsyringe';
import {UserServiceImpl} from './service';

export default (container: DependencyContainer) => {
container.register('UserService', { useClass: UserServiceImpl })
}

+ 147
- 0
packages/service-core/src/modules/user/service.ts Näytä tiedosto

@@ -0,0 +1,147 @@
import {inject, singleton} from 'tsyringe'
import {PrismaClient} from '@prisma/client'
import { models } from '@tonality/library-common'
import Uuid from '@tonality/library-uuid'
import PasswordService from '../password/service'

export default interface UserService {
get(id: Uuid): Promise<models.UserProfile>
getByUsername(username: string): Promise<models.UserProfile>
browse(skip?: number, take?: number): Promise<models.UserProfile[]>
search(q?: string): Promise<models.UserProfile[]>
create(profile: Partial<models.UserProfile>, username: string, newPassword: string, confirmNewPassword: string): Promise<models.UserProfile>
updateProfile(profile: Partial<models.UserProfile>): Promise<models.UserProfile>
updatePassword(id: Uuid, oldPassword: string, newPassword: string, confirmNewPassword: string): Promise<void>
delete(id: Uuid): Promise<void>
}

@singleton()
export class UserServiceImpl implements UserService {
constructor(
@inject('PrismaClient')
private readonly prismaClient: PrismaClient,
@inject('PasswordService')
private readonly passwordService: PasswordService
) {}

async get(id: Uuid): Promise<models.UserProfile> {
const user = await this.prismaClient.userProfile.findUnique({
where: {
userId: id,
},
})

if (!user) {
throw new Error('User not found!')
}

return user
}

async getByUsername(username: string): Promise<models.UserProfile> {
const user = await this.prismaClient.userProfile.findFirst({
where: {
user: {
username,
},
},
})

if (!user) {
throw new Error('User not found!')
}

return user
}

async browse(skip: number = 0, take: number = 10): Promise<models.UserProfile[]> {
return this.prismaClient.userProfile.findMany({
skip,
take,
})
}

async search(q: string = ''): Promise<models.UserProfile[]> {
return this.prismaClient.userProfile.findMany({
where: {
user: {
username: {
contains: q,
},
},
},
})
}

async create(profile: Partial<models.UserProfile>, username: string, newPassword: string, confirmNewPassword: string): Promise<models.UserProfile> {
if (newPassword !== confirmNewPassword) {
throw new Error('Passwords do not match!')
}

const newUserId = Uuid.new()
const password = await this.passwordService.hash(newPassword)

await this.prismaClient.user.create({
data: {
id: newUserId,
username,
password,
},
})

return this.prismaClient.userProfile.create({
data: {
...profile,
userId: newUserId,
},
})
}

async updateProfile(profile: Partial<models.UserProfile>): Promise<models.UserProfile> {
const { userId, ...etcProfile } = profile
return this.prismaClient.userProfile.update({
where: {
userId: userId as Uuid,
},
data: etcProfile,
})
}

async updatePassword(id: Uuid, oldPassword: string, newPassword: string, confirmNewPassword: string): Promise<void> {
const user = await this.prismaClient.user.findUnique({
where: {
id,
},
})

if (!user) {
throw new Error('Invalid request!')
}

const oldPasswordsMatch = await this.passwordService.compare(user.password, oldPassword)
if (!oldPasswordsMatch) {
throw new Error('Invalid request!')
}

if (newPassword !== confirmNewPassword) {
throw new Error('Passwords do not match!')
}
const password = await this.passwordService.hash(newPassword)
await this.prismaClient.user.update({
where: {
id,
},
data: {
password,
},
})
}

async delete(id: Uuid): Promise<void> {
await this.prismaClient.user.delete({
where: {
id,
},
})
}
}

+ 3
- 3
packages/service-core/src/routes/api/ringtones/index.ts Näytä tiedosto

@@ -1,9 +1,9 @@
import {FastifyPluginAsync} from 'fastify'
import ringtoneModule from '../../../modules/ringtone'
import {RingtoneController} from '../../../modules/ringtone/controller'
import {container} from 'tsyringe'
import RingtoneController from '../../../modules/ringtone/controller'

const ringtones: FastifyPluginAsync = async (fastify): Promise<void> => {
const ringtoneController = ringtoneModule.resolve<RingtoneController>('RingtoneController')
const ringtoneController = container.resolve<RingtoneController>('RingtoneController')
fastify.get('/', ringtoneController.browse)
fastify.get('/:id', ringtoneController.get)
fastify.post('/', ringtoneController.create)


+ 3
- 3
packages/service-core/src/routes/api/search/ringtones/index.ts Näytä tiedosto

@@ -1,9 +1,9 @@
import {FastifyPluginAsync} from 'fastify'
import ringtoneModule from '../../../../modules/ringtone'
import {RingtoneController} from '../../../../modules/ringtone/controller'
import {container} from 'tsyringe'
import RingtoneController from '../../../../modules/ringtone/controller'

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



+ 0
- 111
packages/service-core/src/utils/database/index.ts Näytä tiedosto

@@ -1,111 +0,0 @@
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
}
}

+ 12
- 2
packages/service-core/src/utils/helpers/index.ts Näytä tiedosto

@@ -1,4 +1,14 @@
export interface Paginated<T> extends Array<T> {
take: number,
import {FastifyReply, FastifyRequest} from 'fastify';

export interface ControllerResponse<T = unknown> {
data: T
}

export interface PaginatedResponse<T> extends ControllerResponse<T[]> {
skip: number,
take: number,
}

export type Controller<ResponseMap extends Record<string, unknown> = Record<string, unknown>> = {
[T in keyof ResponseMap]: (request: FastifyRequest, reply: FastifyReply) => Promise<ControllerResponse | void>
}

+ 42
- 0
packages/service-core/src/utils/mocks/index.ts Näytä tiedosto

@@ -0,0 +1,42 @@
type CreateArgs = { data: Record<string, any> }

type UpdateArgs = { data: Record<string, any>, where: Record<string, any> }

export class MockPrismaRepository<T extends Record<string, any> = Record<string, any>> {
constructor(private data: T[], private readonly pkAttribute: string) {}

async findMany() {
return this.data
}

async findFirst() {
return this.data[0]
}

async findUnique() {
return this.data[0]
}

async create(args: CreateArgs) {
return args.data
}

async update(args: UpdateArgs) {
let updatedData: T | undefined
this.data = this.data.map(d => {
if (d[this.pkAttribute].toString() === args.where[this.pkAttribute].toString()) {
return (updatedData = {
...d,
...args.data,
} as T)
}
return d
})
if (updatedData) {
return updatedData
}
return args.data
}

async delete() {}
}

+ 0
- 83
packages/service-core/test/mocks/repositories/Ringtone.ts Näytä tiedosto

@@ -1,83 +0,0 @@
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
}
}

+ 63
- 43
packages/service-core/test/routes/api/ringtones.test.ts Näytä tiedosto

@@ -1,46 +1,67 @@
import {FastifyInstance} from 'fastify'
import {PrismaClient as RealPrismaClient} from '@prisma/client'
import {container} from 'tsyringe'
import {models} from '@tonality/library-common'
import Uuid from '@tonality/library-uuid'
import {build} from '../../helper'
import ringtoneModule from '../../../src/modules/ringtone'
import MockRingtoneRepository from '../../mocks/repositories/Ringtone'
import {MockPrismaRepository} from '../../../src/utils/mocks'

describe('ringtone', () => {
jest.mock('@prisma/client')
const PrismaClient = RealPrismaClient as jest.Mock

describe('ringtone: /api/ringtones', () => {
let app: FastifyInstance

beforeEach(() => {
PrismaClient.mockImplementationOnce(() => ({
user: new MockPrismaRepository<models.User>([
{
id: Uuid.parse('00000000-0000-0000-0000-000000000000'),
username: 'TheoryOfNekomata',
password: 'dummy password',
},
], 'id'),
ringtone: new MockPrismaRepository<models.Ringtone>([
{
id: Uuid.parse('00000000-0000-0000-0000-000000000000'),
name: 'Ringtone 1',
data: '4c4',
tempo: 120,
createdAt: new Date('2021-05-01'),
updatedAt: new Date('2021-05-01'),
deletedAt: null,
composerUserId: Uuid.parse('00000000-0000-0000-0000-000000000000'),
},
{
id: Uuid.parse('8df2751c-4c05-4831-9408-92af3b4a63e8'),
name: 'Unique Ringtone 2',
data: '4c4 8c4',
tempo: 120,
createdAt: new Date('2021-05-01'),
updatedAt: new Date('2021-05-01'),
deletedAt: null,
composerUserId: Uuid.parse('00000000-0000-0000-0000-000000000000'),
},
{
id: Uuid.parse('c8423931-6a08-41f6-8320-a5ce5367179f'),
name: 'Deleted Ringtone 1',
data: '4c4',
tempo: 120,
createdAt: new Date('2021-05-01'),
updatedAt: new Date('2021-05-01'),
deletedAt: new Date('2021-05-05'),
composerUserId: Uuid.parse('00000000-0000-0000-0000-000000000000'),
},
], 'id')
}))
})

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: 'Unique 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()
container.clearInstances()
await app.close()
})

@@ -80,14 +101,15 @@ describe('ringtone', () => {
payload: JSON.stringify({
name: 'New Ringtone',
data: '4c4',
composerId: '00000000-0000-0000-000000000000',
tempo: 120,
composerUserId: '00000000-0000-0000-0000-000000000000',
})
})
const parsedPayload = JSON.parse(res.payload)
expect(parsedPayload.data).toEqual(expect.objectContaining({
name: 'New Ringtone',
data: '4c4',
composerId: '00000000-0000-0000-000000000000',
tempo: 120,
}))
})
})
@@ -95,7 +117,7 @@ describe('ringtone', () => {
describe('on updating', () => {
it('should store the updated data', async () => {
const res = await app.inject({
url: '/api/ringtones/00000000-0000-0000-000000000000',
url: '/api/ringtones/00000000-0000-0000-0000-000000000000',
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
@@ -103,14 +125,14 @@ describe('ringtone', () => {
payload: JSON.stringify({
name: 'Updated Ringtone',
data: '4c8',
composerId: '00000000-0000-0000-000000000000',
tempo: 120,
})
})
const parsedPayload = JSON.parse(res.payload)
expect(parsedPayload.data).toEqual(expect.objectContaining({
name: 'Updated Ringtone',
data: '4c8',
composerId: '00000000-0000-0000-000000000000',
tempo: 120,
}))
expect(parsedPayload.data.createdAt).not.toEqual(parsedPayload.data.updatedAt)
})
@@ -119,12 +141,11 @@ describe('ringtone', () => {
describe('on soft deletion', () => {
it('should be tagged as deleted', async () => {
const res = await app.inject({
url: '/api/ringtones/00000000-0000-0000-000000000000/delete',
url: '/api/ringtones/00000000-0000-0000-0000-000000000000/delete',
method: 'POST',
})
const parsedPayload = JSON.parse(res.payload)
expect(parsedPayload.data).toEqual(expect.objectContaining({
id: '00000000-0000-0000-000000000000',
deletedAt: expect.any(String),
}))
})
@@ -133,12 +154,11 @@ describe('ringtone', () => {
describe('on undoing deletion', () => {
it('should be untagged as deleted', async () => {
const res = await app.inject({
url: '/api/ringtones/00000000-0000-0000-000000000000/delete',
url: '/api/ringtones/00000000-0000-0000-0000-000000000000/delete',
method: 'DELETE',
})
const parsedPayload = JSON.parse(res.payload)
expect(parsedPayload.data).toEqual(expect.objectContaining({
id: '00000000-0000-0000-000000000000',
deletedAt: null,
}))
})
@@ -147,7 +167,7 @@ describe('ringtone', () => {
describe('on hard deletion', () => {
it('should be removed', async () => {
const res = await app.inject({
url: '/api/ringtones/00000000-0000-0000-000000000000',
url: '/api/ringtones/00000000-0000-0000-0000-000000000000',
method: 'DELETE',
})
expect(res.statusCode).toBe(204)


+ 69
- 0
packages/service-core/test/routes/api/search/ringtones.test.ts Näytä tiedosto

@@ -0,0 +1,69 @@
import {FastifyInstance} from 'fastify';
import Uuid from '@tonality/library-uuid';
import {models} from '@tonality/library-common';
import {PrismaClient as RealPrismaClient} from '@prisma/client';
import {MockPrismaRepository} from '../../../../src/utils/mocks';
import {container} from 'tsyringe';
import {build} from '../../../helper';

jest.mock('@prisma/client')
const PrismaClient = RealPrismaClient as jest.Mock

describe('ringtone: /api/search/ringtones', () => {
let app: FastifyInstance

beforeEach(() => {
PrismaClient.mockImplementationOnce(() => ({
user: new MockPrismaRepository<models.User>([
{
id: Uuid.parse('00000000-0000-0000-0000-000000000000'),
username: 'TheoryOfNekomata',
password: 'dummy password',
},
], 'id'),
ringtone: new MockPrismaRepository<models.Ringtone>([
{
id: Uuid.parse('00000000-0000-0000-0000-000000000000'),
name: 'Ringtone 1',
data: '4c4',
tempo: 120,
createdAt: new Date('2021-05-01'),
updatedAt: new Date('2021-05-01'),
deletedAt: null,
composerUserId: Uuid.parse('00000000-0000-0000-0000-000000000000'),
},
{
id: Uuid.parse('8df2751c-4c05-4831-9408-92af3b4a63e8'),
name: 'Unique Ringtone 2',
data: '4c4 8c4',
tempo: 120,
createdAt: new Date('2021-05-01'),
updatedAt: new Date('2021-05-01'),
deletedAt: null,
composerUserId: Uuid.parse('00000000-0000-0000-0000-000000000000'),
},
{
id: Uuid.parse('c8423931-6a08-41f6-8320-a5ce5367179f'),
name: 'Deleted Ringtone 1',
data: '4c4',
tempo: 120,
createdAt: new Date('2021-05-01'),
updatedAt: new Date('2021-05-01'),
deletedAt: new Date('2021-05-05'),
composerUserId: Uuid.parse('00000000-0000-0000-0000-000000000000'),
},
], 'id')
}))
})

beforeEach(async () => {
app = await build()
})

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

it.todo('should foo')
})

+ 132
- 7
packages/service-core/yarn.lock Näytä tiedosto

@@ -500,6 +500,38 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz#2a0b32fcb416fb3f2250fd24cb2a81421a4f5950"
integrity sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==
dependencies:
detect-libc "^1.0.3"
https-proxy-agent "^5.0.0"
make-dir "^3.1.0"
node-fetch "^2.6.1"
nopt "^5.0.0"
npmlog "^4.1.2"
rimraf "^3.0.2"
semver "^7.3.4"
tar "^6.1.0"
"@prisma/client@^2.24.1":
version "2.24.1"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.24.1.tgz#c4f26fb4d768dd52dd20a17e626f10e69cc0b85c"
integrity sha512-vllhf36g3oI98GF1Q5IPmnR5MYzBPeCcl/Xiz6EAi4DMOxE069o9ka5BAqYbUG2USx8JuKw09QdMnDrp3Kyn8g==
dependencies:
"@prisma/engines-version" "2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4"
"@prisma/engines-version@2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4":
version "2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4.tgz#2c5813ef98bcbe659b18b521f002f5c8aabbaae2"
integrity sha512-60Do+ByVfHnhJ2id5h/lXOZnDQNIf5pz3enkKWOmyr744Z2IxkBu65jRckFfMN5cPtmXDre/Ay/GKm0aoeLwrw==
"@prisma/engines@2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4":
version "2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4.tgz#7e542d510f0c03f41b73edbb17254f5a0b272a4d"
integrity sha512-29/xO9kqeQka+wN5Ev10l5L4XQXNVXdPToJs1M29VZ2imQsNsL4rtz26m3qGM54IoGWwwfTVdvuVRxKnDl2rig==
"@sinonjs/commons@^1.7.0":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
@@ -547,6 +579,13 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/bcrypt@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-5.0.0.tgz#a835afa2882d165aff5690893db314eaa98b9f20"
integrity sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==
dependencies:
"@types/node" "*"
"@types/graceful-fs@^4.1.2":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
@@ -663,6 +702,13 @@ acorn@^8.1.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.2.4.tgz#caba24b08185c3b56e3168e97d15ed17f4d31fd0"
integrity sha512-Ibt84YwBDDA890eDiDCEqcbwvHlBvzzDkU2cGBBDDI1QWT12jTiXIOn2CIw5KK4i6N5Z2HUxwYjzriDyqaqqZg==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
ajv@^6.11.0, ajv@^6.12.2, ajv@^6.12.3:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@@ -925,6 +971,14 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
bcrypt@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71"
integrity sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.0"
node-addon-api "^3.1.0"
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
@@ -1159,6 +1213,11 @@ chownr@^1.1.1:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
@@ -1408,7 +1467,7 @@ date-fns@^2.16.1:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.3.tgz#8f5f6889d7a96bbcc1f0ea50239b397a83357f9b"
integrity sha512-HeYdzCaFflc1i4tGbj7JKMjM4cKGYoyxwcIIkHzNgCkX8xXDNJDZXgDDVchIWpN4eQc3lH37WarduXFZJOtxfw==
debug@4.3.1, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1:
debug@4, debug@4.3.1, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
@@ -1496,7 +1555,7 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
detect-libc@^1.0.2:
detect-libc@^1.0.2, detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
@@ -1976,6 +2035,13 @@ fs-minipass@^1.2.5:
dependencies:
minipass "^2.6.0"
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
dependencies:
minipass "^3.0.0"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -2233,6 +2299,14 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
dependencies:
agent-base "6"
debug "4"
human-signals@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
@@ -3129,7 +3203,7 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
make-dir@^3.0.0:
make-dir@^3.0.0, make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@@ -3276,6 +3350,13 @@ minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
safe-buffer "^5.1.2"
yallist "^3.0.0"
minipass@^3.0.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
dependencies:
yallist "^4.0.0"
minizlib@^1.2.1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
@@ -3283,6 +3364,14 @@ minizlib@^1.2.1:
dependencies:
minipass "^2.9.0"
minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
dependencies:
minipass "^3.0.0"
yallist "^4.0.0"
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
@@ -3291,7 +3380,7 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
mkdirp@1.x:
mkdirp@1.x, mkdirp@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
@@ -3359,6 +3448,11 @@ node-addon-api@^3.0.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239"
integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==
node-addon-api@^3.1.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
node-emoji@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da"
@@ -3366,6 +3460,11 @@ node-emoji@^1.10.0:
dependencies:
lodash.toarray "^4.4.0"
node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-gyp@3.x:
version "3.8.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
@@ -3442,6 +3541,13 @@ nopt@^4.0.1:
abbrev "1"
osenv "^0.1.4"
nopt@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
dependencies:
abbrev "1"
nopt@~2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-2.1.2.tgz#6cccd977b80132a07731d6e8ce58c2c8303cf9af"
@@ -3506,7 +3612,7 @@ npm-run-path@^4.0.0:
dependencies:
path-key "^3.0.0"
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2:
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2, npmlog@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -3837,6 +3943,13 @@ pretty-ms@^5.0.0:
dependencies:
parse-ms "^2.1.0"
prisma@^2.24.1:
version "2.24.1"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.24.1.tgz#f8f4cb8baf407a71800256160277f69603bd43a3"
integrity sha512-L+ykMpttbWzpTNsy+PPynnEX/mS1s5zs49euXBrMjxXh1M6/f9MYlTNAj+iP90O9ZSaURSpNa+1jzatPghqUcQ==
dependencies:
"@prisma/engines" "2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4"
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -4097,7 +4210,7 @@ rimraf@2, rimraf@^2.6.1:
dependencies:
glob "^7.1.3"
rimraf@^3.0.0:
rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
@@ -4187,7 +4300,7 @@ semver-store@^0.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
semver@7.x, semver@^7.3.2:
semver@7.x, semver@^7.3.2, semver@^7.3.4:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
@@ -4618,6 +4731,18 @@ tar@^4:
safe-buffer "^5.1.2"
yallist "^3.0.3"
tar@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^3.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
tarn@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec"


Ladataan…
Peruuta
Tallenna