@@ -1,14 +1,16 @@ | |||||
import Uuid from '@tonality/library-uuid' | |||||
export namespace models { | export namespace models { | ||||
export class Composer { | |||||
id?: string | |||||
export class User { | |||||
id: Uuid | |||||
name: string | |||||
username: string | |||||
bio?: string | |||||
password: string | |||||
} | } | ||||
export class Ringtone { | export class Ringtone { | ||||
id?: string | |||||
id: Uuid | |||||
name: string | name: string | ||||
@@ -20,8 +22,16 @@ export namespace models { | |||||
updatedAt: Date | updatedAt: Date | ||||
deletedAt?: Date | |||||
deletedAt?: Date | null | |||||
composerUserId: Uuid | |||||
} | |||||
export class UserProfile { | |||||
userId: Uuid | |||||
bio?: string | null | |||||
composerId: string | |||||
email?: string | null | |||||
} | } | ||||
} | } |
@@ -0,0 +1,4 @@ | |||||
*.log | |||||
.DS_Store | |||||
node_modules | |||||
dist |
@@ -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. |
@@ -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). |
@@ -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" | |||||
} | |||||
} |
@@ -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 | |||||
} | |||||
} |
@@ -0,0 +1,7 @@ | |||||
import { sum } from '../src'; | |||||
describe('blah', () => { | |||||
it('works', () => { | |||||
expect(sum(1, 1)).toEqual(2); | |||||
}); | |||||
}); |
@@ -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, | |||||
} | |||||
} |
@@ -1,8 +1,6 @@ | |||||
PORT= | PORT= | ||||
DATABASE_TYPE= | |||||
DATABASE_LOCATION= | |||||
DATABASE_URL= | |||||
DATABASE_USERNAME= | DATABASE_USERNAME= | ||||
@@ -11,3 +9,5 @@ DATABASE_PASSWORD= | |||||
DATABASE_SCHEMA= | DATABASE_SCHEMA= | ||||
APP_BASE_URL= | APP_BASE_URL= | ||||
PASSWORD_GEN_SALT_ROUNDS= |
@@ -1,3 +1,4 @@ | |||||
import '@abraham/reflection' | |||||
import { config } from 'dotenv' | import { config } from 'dotenv' | ||||
config() | config() |
@@ -17,6 +17,8 @@ | |||||
"license": "MIT", | "license": "MIT", | ||||
"dependencies": { | "dependencies": { | ||||
"@abraham/reflection": "^0.8.0", | "@abraham/reflection": "^0.8.0", | ||||
"@prisma/client": "^2.24.1", | |||||
"bcrypt": "^5.0.1", | |||||
"dotenv": "^9.0.2", | "dotenv": "^9.0.2", | ||||
"fastify": "^3.0.0", | "fastify": "^3.0.0", | ||||
"fastify-autoload": "^3.3.1", | "fastify-autoload": "^3.3.1", | ||||
@@ -29,6 +31,7 @@ | |||||
"uuid": "^8.3.2" | "uuid": "^8.3.2" | ||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@types/bcrypt": "^5.0.0", | |||||
"@types/jest": "^26.0.23", | "@types/jest": "^26.0.23", | ||||
"@types/node": "^15.0.0", | "@types/node": "^15.0.0", | ||||
"@types/uuid": "^8.3.0", | "@types/uuid": "^8.3.0", | ||||
@@ -36,6 +39,7 @@ | |||||
"cross-env": "^7.0.3", | "cross-env": "^7.0.3", | ||||
"fastify-tsconfig": "^1.0.1", | "fastify-tsconfig": "^1.0.1", | ||||
"jest": "^26.6.3", | "jest": "^26.6.3", | ||||
"prisma": "^2.24.1", | |||||
"ts-jest": "^26.5.6", | "ts-jest": "^26.5.6", | ||||
"typescript": "^4.1.3" | "typescript": "^4.1.3" | ||||
}, | }, | ||||
@@ -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") | |||||
} |
@@ -3,6 +3,7 @@ import '@abraham/reflection' | |||||
import {join} from 'path' | import {join} from 'path' | ||||
import AutoLoad, {AutoloadPluginOptions} from 'fastify-autoload' | import AutoLoad, {AutoloadPluginOptions} from 'fastify-autoload' | ||||
import {FastifyPluginAsync} from 'fastify' | import {FastifyPluginAsync} from 'fastify' | ||||
import {container} from 'tsyringe'; | |||||
export type AppOptions = { | export type AppOptions = { | ||||
// Place your custom options for app below here. | // Place your custom options for app below here. | ||||
@@ -13,6 +14,14 @@ const app: FastifyPluginAsync<AppOptions> = async ( | |||||
opts, | opts, | ||||
): Promise<void> => { | ): Promise<void> => { | ||||
// Place here your custom code! | // 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 | // Do not touch the following lines | ||||
@@ -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 }) | |||||
} |
@@ -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> | |||||
} |
@@ -0,0 +1,6 @@ | |||||
import {DependencyContainer} from 'tsyringe'; | |||||
import {PasswordServiceImpl} from './service'; | |||||
export default (container: DependencyContainer) => { | |||||
container.register('PasswordService', { useClass: PasswordServiceImpl }) | |||||
} |
@@ -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) | |||||
} | |||||
} |
@@ -1,18 +1,34 @@ | |||||
import {inject, singleton} from 'tsyringe' | 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' | 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() | @singleton() | ||||
export class RingtoneController { | |||||
export class RingtoneControllerImpl implements RingtoneController { | |||||
constructor( | constructor( | ||||
@inject('RingtoneService') | @inject('RingtoneService') | ||||
private readonly ringtoneService: RingtoneService | |||||
) { | |||||
} | |||||
private readonly ringtoneService: RingtoneService, | |||||
) {} | |||||
get = async (request: any, reply: any) => { | get = async (request: any, reply: any) => { | ||||
try { | 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') { | if (typeof (data.deletedAt as Date) !== 'undefined') { | ||||
reply.raw.statusMessage = 'Ringtone Deleted Previously' | reply.raw.statusMessage = 'Ringtone Deleted Previously' | ||||
reply.gone() | reply.gone() | ||||
@@ -40,8 +56,8 @@ export class RingtoneController { | |||||
const skipNumber = Number(skipRaw) | const skipNumber = Number(skipRaw) | ||||
const takeNumber = Number(takeRaw) | 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' | reply.raw.statusMessage = 'Multiple Ringtones Retrieved' | ||||
return { | return { | ||||
@@ -61,7 +77,6 @@ export class RingtoneController { | |||||
reply.raw.statusMessage = 'Search Results Retrieved' | reply.raw.statusMessage = 'Search Results Retrieved' | ||||
return { | return { | ||||
data: await this.ringtoneService.search(query), | data: await this.ringtoneService.search(query), | ||||
query, | |||||
} | } | ||||
} catch (err) { | } catch (err) { | ||||
reply.raw.statusMessage = 'Search Error' | reply.raw.statusMessage = 'Search Error' | ||||
@@ -71,7 +86,11 @@ export class RingtoneController { | |||||
create = async (request: any, reply: any) => { | create = async (request: any, reply: any) => { | ||||
try { | 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.statusMessage = 'Ringtone Created' | ||||
reply.raw.statusCode = 201 | reply.raw.statusCode = 201 | ||||
return { | return { | ||||
@@ -86,11 +105,12 @@ export class RingtoneController { | |||||
update = async (request: any, reply: any) => { | update = async (request: any, reply: any) => { | ||||
try { | try { | ||||
// TODO validate data | // TODO validate data | ||||
const id = Uuid.parse(request.params['id']) | |||||
const data = await this.ringtoneService.update({ | const data = await this.ringtoneService.update({ | ||||
...request.body, | ...request.body, | ||||
id: request.params['id'], | |||||
id, | |||||
}) | }) | ||||
if (data.deletedAt !== null) { | |||||
if (data.deletedAt) { | |||||
reply.raw.statusMessage = 'Ringtone Deleted Previously' | reply.raw.statusMessage = 'Ringtone Deleted Previously' | ||||
reply.gone() | reply.gone() | ||||
return | return | ||||
@@ -112,7 +132,8 @@ export class RingtoneController { | |||||
softDelete = async (request: any, reply: any) => { | softDelete = async (request: any, reply: any) => { | ||||
try { | try { | ||||
// TODO validate data | // 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) { | if (!data) { | ||||
reply.raw.statusMessage = 'Ringtone Not Found' | reply.raw.statusMessage = 'Ringtone Not Found' | ||||
reply.notFound() | reply.notFound() | ||||
@@ -131,7 +152,8 @@ export class RingtoneController { | |||||
undoDelete = async (request: any, reply: any) => { | undoDelete = async (request: any, reply: any) => { | ||||
try { | try { | ||||
// TODO validate data | // 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) { | if (!data) { | ||||
reply.raw.statusMessage = 'Ringtone Not Found' | reply.raw.statusMessage = 'Ringtone Not Found' | ||||
reply.notFound() | reply.notFound() | ||||
@@ -150,7 +172,8 @@ export class RingtoneController { | |||||
hardDelete = async (request: any, reply: any) => { | hardDelete = async (request: any, reply: any) => { | ||||
try { | try { | ||||
// TODO validate data | // 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.status(204) | ||||
reply.raw.statusMessage = 'Ringtone Hard-Deleted' | reply.raw.statusMessage = 'Ringtone Hard-Deleted' | ||||
} catch (err) { | } catch (err) { | ||||
@@ -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 }) | |||||
} |
@@ -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 | |||||
} | |||||
} | |||||
) | |||||
}) | |||||
}) | |||||
} | |||||
} |
@@ -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('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() | |||||
}) | }) | ||||
}) | }) | ||||
}) | }) |
@@ -1,61 +1,109 @@ | |||||
import {inject, singleton} from 'tsyringe' | 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() | @singleton() | ||||
export class RingtoneService { | |||||
export class RingtoneServiceImpl { | |||||
constructor( | 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[]> { | 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[]> { | 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> { | 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> { | async update(data: Partial<models.Ringtone>): Promise<models.Ringtone> { | ||||
const { createdAt, updatedAt, deletedAt, ...safeData } = data | 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, | |||||
}, | |||||
}) | |||||
} | } | ||||
} | } |
@@ -0,0 +1,6 @@ | |||||
import {DependencyContainer} from 'tsyringe'; | |||||
import {UserServiceImpl} from './service'; | |||||
export default (container: DependencyContainer) => { | |||||
container.register('UserService', { useClass: UserServiceImpl }) | |||||
} |
@@ -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, | |||||
}, | |||||
}) | |||||
} | |||||
} |
@@ -1,9 +1,9 @@ | |||||
import {FastifyPluginAsync} from 'fastify' | 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 ringtones: FastifyPluginAsync = async (fastify): Promise<void> => { | ||||
const ringtoneController = ringtoneModule.resolve<RingtoneController>('RingtoneController') | |||||
const ringtoneController = container.resolve<RingtoneController>('RingtoneController') | |||||
fastify.get('/', ringtoneController.browse) | fastify.get('/', ringtoneController.browse) | ||||
fastify.get('/:id', ringtoneController.get) | fastify.get('/:id', ringtoneController.get) | ||||
fastify.post('/', ringtoneController.create) | fastify.post('/', ringtoneController.create) | ||||
@@ -1,9 +1,9 @@ | |||||
import {FastifyPluginAsync} from 'fastify' | 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 ringtones: FastifyPluginAsync = async (fastify): Promise<void> => { | ||||
const ringtoneController = ringtoneModule.resolve<RingtoneController>('RingtoneController') | |||||
const ringtoneController = container.resolve<RingtoneController>('RingtoneController') | |||||
fastify.get('/', ringtoneController.search) | fastify.get('/', ringtoneController.search) | ||||
} | } | ||||
@@ -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 | |||||
} | |||||
} |
@@ -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, | 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> | |||||
} | } |
@@ -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() {} | |||||
} |
@@ -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 | |||||
} | |||||
} |
@@ -1,46 +1,67 @@ | |||||
import {FastifyInstance} from 'fastify' | 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 {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 | 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 () => { | 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() | app = await build() | ||||
}) | }) | ||||
afterEach(async () => { | afterEach(async () => { | ||||
ringtoneModule.clearInstances() | |||||
container.clearInstances() | |||||
await app.close() | await app.close() | ||||
}) | }) | ||||
@@ -80,14 +101,15 @@ describe('ringtone', () => { | |||||
payload: JSON.stringify({ | payload: JSON.stringify({ | ||||
name: 'New Ringtone', | name: 'New Ringtone', | ||||
data: '4c4', | data: '4c4', | ||||
composerId: '00000000-0000-0000-000000000000', | |||||
tempo: 120, | |||||
composerUserId: '00000000-0000-0000-0000-000000000000', | |||||
}) | }) | ||||
}) | }) | ||||
const parsedPayload = JSON.parse(res.payload) | const parsedPayload = JSON.parse(res.payload) | ||||
expect(parsedPayload.data).toEqual(expect.objectContaining({ | expect(parsedPayload.data).toEqual(expect.objectContaining({ | ||||
name: 'New Ringtone', | name: 'New Ringtone', | ||||
data: '4c4', | data: '4c4', | ||||
composerId: '00000000-0000-0000-000000000000', | |||||
tempo: 120, | |||||
})) | })) | ||||
}) | }) | ||||
}) | }) | ||||
@@ -95,7 +117,7 @@ describe('ringtone', () => { | |||||
describe('on updating', () => { | describe('on updating', () => { | ||||
it('should store the updated data', async () => { | it('should store the updated data', async () => { | ||||
const res = await app.inject({ | const res = await app.inject({ | ||||
url: '/api/ringtones/00000000-0000-0000-000000000000', | |||||
url: '/api/ringtones/00000000-0000-0000-0000-000000000000', | |||||
method: 'PATCH', | method: 'PATCH', | ||||
headers: { | headers: { | ||||
'Content-Type': 'application/json', | 'Content-Type': 'application/json', | ||||
@@ -103,14 +125,14 @@ describe('ringtone', () => { | |||||
payload: JSON.stringify({ | payload: JSON.stringify({ | ||||
name: 'Updated Ringtone', | name: 'Updated Ringtone', | ||||
data: '4c8', | data: '4c8', | ||||
composerId: '00000000-0000-0000-000000000000', | |||||
tempo: 120, | |||||
}) | }) | ||||
}) | }) | ||||
const parsedPayload = JSON.parse(res.payload) | const parsedPayload = JSON.parse(res.payload) | ||||
expect(parsedPayload.data).toEqual(expect.objectContaining({ | expect(parsedPayload.data).toEqual(expect.objectContaining({ | ||||
name: 'Updated Ringtone', | name: 'Updated Ringtone', | ||||
data: '4c8', | data: '4c8', | ||||
composerId: '00000000-0000-0000-000000000000', | |||||
tempo: 120, | |||||
})) | })) | ||||
expect(parsedPayload.data.createdAt).not.toEqual(parsedPayload.data.updatedAt) | expect(parsedPayload.data.createdAt).not.toEqual(parsedPayload.data.updatedAt) | ||||
}) | }) | ||||
@@ -119,12 +141,11 @@ describe('ringtone', () => { | |||||
describe('on soft deletion', () => { | describe('on soft deletion', () => { | ||||
it('should be tagged as deleted', async () => { | it('should be tagged as deleted', async () => { | ||||
const res = await app.inject({ | const res = await app.inject({ | ||||
url: '/api/ringtones/00000000-0000-0000-000000000000/delete', | |||||
url: '/api/ringtones/00000000-0000-0000-0000-000000000000/delete', | |||||
method: 'POST', | method: 'POST', | ||||
}) | }) | ||||
const parsedPayload = JSON.parse(res.payload) | const parsedPayload = JSON.parse(res.payload) | ||||
expect(parsedPayload.data).toEqual(expect.objectContaining({ | expect(parsedPayload.data).toEqual(expect.objectContaining({ | ||||
id: '00000000-0000-0000-000000000000', | |||||
deletedAt: expect.any(String), | deletedAt: expect.any(String), | ||||
})) | })) | ||||
}) | }) | ||||
@@ -133,12 +154,11 @@ describe('ringtone', () => { | |||||
describe('on undoing deletion', () => { | describe('on undoing deletion', () => { | ||||
it('should be untagged as deleted', async () => { | it('should be untagged as deleted', async () => { | ||||
const res = await app.inject({ | const res = await app.inject({ | ||||
url: '/api/ringtones/00000000-0000-0000-000000000000/delete', | |||||
url: '/api/ringtones/00000000-0000-0000-0000-000000000000/delete', | |||||
method: 'DELETE', | method: 'DELETE', | ||||
}) | }) | ||||
const parsedPayload = JSON.parse(res.payload) | const parsedPayload = JSON.parse(res.payload) | ||||
expect(parsedPayload.data).toEqual(expect.objectContaining({ | expect(parsedPayload.data).toEqual(expect.objectContaining({ | ||||
id: '00000000-0000-0000-000000000000', | |||||
deletedAt: null, | deletedAt: null, | ||||
})) | })) | ||||
}) | }) | ||||
@@ -147,7 +167,7 @@ describe('ringtone', () => { | |||||
describe('on hard deletion', () => { | describe('on hard deletion', () => { | ||||
it('should be removed', async () => { | it('should be removed', async () => { | ||||
const res = await app.inject({ | const res = await app.inject({ | ||||
url: '/api/ringtones/00000000-0000-0000-000000000000', | |||||
url: '/api/ringtones/00000000-0000-0000-0000-000000000000', | |||||
method: 'DELETE', | method: 'DELETE', | ||||
}) | }) | ||||
expect(res.statusCode).toBe(204) | expect(res.statusCode).toBe(204) | ||||
@@ -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') | |||||
}) |
@@ -500,6 +500,38 @@ | |||||
"@types/yargs" "^15.0.0" | "@types/yargs" "^15.0.0" | ||||
chalk "^4.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": | "@sinonjs/commons@^1.7.0": | ||||
version "1.8.3" | version "1.8.3" | ||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" | resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" | ||||
@@ -547,6 +579,13 @@ | |||||
dependencies: | dependencies: | ||||
"@babel/types" "^7.3.0" | "@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": | "@types/graceful-fs@^4.1.2": | ||||
version "4.1.5" | version "4.1.5" | ||||
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" | 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" | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.2.4.tgz#caba24b08185c3b56e3168e97d15ed17f4d31fd0" | ||||
integrity sha512-Ibt84YwBDDA890eDiDCEqcbwvHlBvzzDkU2cGBBDDI1QWT12jTiXIOn2CIw5KK4i6N5Z2HUxwYjzriDyqaqqZg== | 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: | ajv@^6.11.0, ajv@^6.12.2, ajv@^6.12.3: | ||||
version "6.12.6" | version "6.12.6" | ||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" | ||||
@@ -925,6 +971,14 @@ bcrypt-pbkdf@^1.0.0: | |||||
dependencies: | dependencies: | ||||
tweetnacl "^0.14.3" | 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: | binary-extensions@^2.0.0: | ||||
version "2.2.0" | version "2.2.0" | ||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" | 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" | resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" | ||||
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== | 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: | ci-info@^2.0.0: | ||||
version "2.0.0" | version "2.0.0" | ||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" | 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" | resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.3.tgz#8f5f6889d7a96bbcc1f0ea50239b397a83357f9b" | ||||
integrity sha512-HeYdzCaFflc1i4tGbj7JKMjM4cKGYoyxwcIIkHzNgCkX8xXDNJDZXgDDVchIWpN4eQc3lH37WarduXFZJOtxfw== | 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" | version "4.3.1" | ||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" | ||||
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== | integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== | ||||
@@ -1496,7 +1555,7 @@ depd@~1.1.2: | |||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" | ||||
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= | integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= | ||||
detect-libc@^1.0.2: | |||||
detect-libc@^1.0.2, detect-libc@^1.0.3: | |||||
version "1.0.3" | version "1.0.3" | ||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" | resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" | ||||
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= | integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= | ||||
@@ -1976,6 +2035,13 @@ fs-minipass@^1.2.5: | |||||
dependencies: | dependencies: | ||||
minipass "^2.6.0" | 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: | fs.realpath@^1.0.0: | ||||
version "1.0.0" | version "1.0.0" | ||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" | 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" | jsprim "^1.2.2" | ||||
sshpk "^1.7.0" | 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: | human-signals@^1.1.1: | ||||
version "1.1.1" | version "1.1.1" | ||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" | resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" | ||||
@@ -3129,7 +3203,7 @@ lru-cache@^6.0.0: | |||||
dependencies: | dependencies: | ||||
yallist "^4.0.0" | yallist "^4.0.0" | ||||
make-dir@^3.0.0: | |||||
make-dir@^3.0.0, make-dir@^3.1.0: | |||||
version "3.1.0" | version "3.1.0" | ||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" | ||||
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== | 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" | safe-buffer "^5.1.2" | ||||
yallist "^3.0.0" | 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: | minizlib@^1.2.1: | ||||
version "1.3.3" | version "1.3.3" | ||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" | resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" | ||||
@@ -3283,6 +3364,14 @@ minizlib@^1.2.1: | |||||
dependencies: | dependencies: | ||||
minipass "^2.9.0" | 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: | mixin-deep@^1.2.0: | ||||
version "1.3.2" | version "1.3.2" | ||||
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" | 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" | for-in "^1.0.2" | ||||
is-extendable "^1.0.1" | is-extendable "^1.0.1" | ||||
mkdirp@1.x: | |||||
mkdirp@1.x, mkdirp@^1.0.3: | |||||
version "1.0.4" | version "1.0.4" | ||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" | ||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== | 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" | resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" | ||||
integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw== | 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: | node-emoji@^1.10.0: | ||||
version "1.10.0" | version "1.10.0" | ||||
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" | resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" | ||||
@@ -3366,6 +3460,11 @@ node-emoji@^1.10.0: | |||||
dependencies: | dependencies: | ||||
lodash.toarray "^4.4.0" | 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: | node-gyp@3.x: | ||||
version "3.8.0" | version "3.8.0" | ||||
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" | resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" | ||||
@@ -3442,6 +3541,13 @@ nopt@^4.0.1: | |||||
abbrev "1" | abbrev "1" | ||||
osenv "^0.1.4" | 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: | nopt@~2.1.2: | ||||
version "2.1.2" | version "2.1.2" | ||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-2.1.2.tgz#6cccd977b80132a07731d6e8ce58c2c8303cf9af" | resolved "https://registry.yarnpkg.com/nopt/-/nopt-2.1.2.tgz#6cccd977b80132a07731d6e8ce58c2c8303cf9af" | ||||
@@ -3506,7 +3612,7 @@ npm-run-path@^4.0.0: | |||||
dependencies: | dependencies: | ||||
path-key "^3.0.0" | 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" | version "4.1.2" | ||||
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" | resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" | ||||
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== | integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== | ||||
@@ -3837,6 +3943,13 @@ pretty-ms@^5.0.0: | |||||
dependencies: | dependencies: | ||||
parse-ms "^2.1.0" | 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: | process-nextick-args@~2.0.0: | ||||
version "2.0.1" | version "2.0.1" | ||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" | 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: | dependencies: | ||||
glob "^7.1.3" | glob "^7.1.3" | ||||
rimraf@^3.0.0: | |||||
rimraf@^3.0.0, rimraf@^3.0.2: | |||||
version "3.0.2" | version "3.0.2" | ||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" | ||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== | 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" | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" | ||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== | 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" | version "7.3.5" | ||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" | resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" | ||||
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== | integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== | ||||
@@ -4618,6 +4731,18 @@ tar@^4: | |||||
safe-buffer "^5.1.2" | safe-buffer "^5.1.2" | ||||
yallist "^3.0.3" | 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: | tarn@^3.0.1: | ||||
version "3.0.1" | version "3.0.1" | ||||
resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec" | resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec" | ||||