@@ -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 | |||
} | |||
} |
@@ -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= | |||
DATABASE_TYPE= | |||
DATABASE_LOCATION= | |||
DATABASE_URL= | |||
DATABASE_USERNAME= | |||
@@ -11,3 +9,5 @@ DATABASE_PASSWORD= | |||
DATABASE_SCHEMA= | |||
APP_BASE_URL= | |||
PASSWORD_GEN_SALT_ROUNDS= |
@@ -1,3 +1,4 @@ | |||
import '@abraham/reflection' | |||
import { config } from 'dotenv' | |||
config() |
@@ -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" | |||
}, | |||
@@ -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 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 | |||
@@ -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 {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) { | |||
@@ -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('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 {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, | |||
}, | |||
}) | |||
} | |||
} |
@@ -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 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) | |||
@@ -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) | |||
} | |||
@@ -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, | |||
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 {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) | |||
@@ -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" | |||
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" | |||