Selaa lähdekoodia

Improve serialization, integrate auth

The UUID library now uses overrides instead of the inherited Buffer
methods for better deserialization.

TODO: add conversions across layers from database to controller.

There is initial effort in implementing Auth0.
master
TheoryOfNekomata 3 vuotta sitten
vanhempi
commit
90e7668c2c
28 muutettua tiedostoa jossa 562 lisäystä ja 89 poistoa
  1. +5
    -4
      REQUIREMENTS.md
  2. +2
    -0
      SAMPLES.md
  3. +16
    -10
      packages/app-web/src/components/organisms/forms/CreateRingtone/index.tsx
  4. +21
    -5
      packages/app-web/src/components/templates/CreateRingtone/index.tsx
  5. +6
    -1
      packages/app-web/src/modules/ringtone/client.ts
  6. +3
    -0
      packages/app-web/src/modules/ringtone/endpoints.ts
  7. +3
    -1
      packages/app-web/src/pages/my/create/ringtone/index.tsx
  8. +2
    -2
      packages/app-web/src/pages/ringtones/index.tsx
  9. +1
    -1
      packages/library-song-utils/src/index.ts
  10. +29
    -5
      packages/library-uuid/src/index.ts
  11. +0
    -2
      packages/service-core/.env.example
  12. +4
    -2
      packages/service-core/package.json
  13. +1
    -1
      packages/service-core/prisma/schema.prisma
  14. +2
    -2
      packages/service-core/src/app.ts
  15. +30
    -0
      packages/service-core/src/modules/auth/controller.ts
  16. +8
    -0
      packages/service-core/src/modules/auth/index.ts
  17. +37
    -0
      packages/service-core/src/modules/auth/service.ts
  18. +6
    -0
      packages/service-core/src/modules/credentials/index.ts
  19. +34
    -0
      packages/service-core/src/modules/credentials/service.ts
  20. +6
    -0
      packages/service-core/src/modules/credentials/type.ts
  21. +28
    -10
      packages/service-core/src/modules/ringtone/controller.ts
  22. +43
    -11
      packages/service-core/src/modules/ringtone/service.ts
  23. +17
    -2
      packages/service-core/src/modules/user/service.ts
  24. +9
    -0
      packages/service-core/src/plugins/auth0-verify.ts
  25. +14
    -0
      packages/service-core/src/routes/api/auth/index.ts
  26. +46
    -7
      packages/service-core/src/routes/api/ringtones/index.ts
  27. +2
    -1
      packages/service-core/tsconfig.json
  28. +187
    -22
      packages/service-core/yarn.lock

+ 5
- 4
REQUIREMENTS.md Näytä tiedosto

@@ -15,12 +15,13 @@
- [ ] In the front-end, the client provides a ringtone ID to request a ringtone from the back-end.
- [ ] In the back-end, the server sends the ringtone data to the front-end.
- As a client, I want to browse ringtones.
- [ ] In the front-end, the client provides an optional skip and take arguments to request multiple ringtones from the
- [ ] In the front-end, the client provides optional skip and take arguments to request multiple ringtones from the
back-end.
- [X] In the back-end, the server sends the ringtones to the front-end.
- As a client, I want to view the ringtones made by a composer.
- [ ] In the front-end
- [ ] In the back-end
- [ ] In the front-end, the client provides a composer's user ID and optional skip and take arguments to request
multiple ringtones from the back-end.
- [X] In the back-end, the server sends the ringtones to the front-end.

## Search

@@ -30,7 +31,7 @@
front-end.
- As a client, I want to search for composers.
- [ ] In the front-end, the client requests provides a search keyword to request for composers from the back-end.
- [ ] In the back-end, the server retrieves the composers whose name matches the search keyword provided by the
- [X] In the back-end, the server retrieves the composers whose name matches the search keyword provided by the
front-end.

## Composer


+ 2
- 0
SAMPLES.md Näytä tiedosto

@@ -0,0 +1,2 @@
Name: Happy Birthday
Data: 8g4 8g4 4a4 4g4 4c5 2b4 8g4 8g4 4a4 4g4 4d5 2c5 8g4 8g4 4g5 4e5 8c5 8c5 4b4 4a4 8f5 8f5 4e5 4c5 4d5 2c5.

+ 16
- 10
packages/app-web/src/components/organisms/forms/CreateRingtone/index.tsx Näytä tiedosto

@@ -1,5 +1,7 @@
import {CSSProperties, FC, FormEventHandler, useEffect, useRef, useState} from 'react';
import {FC, FormEventHandler, useEffect, useRef, useState} from 'react';
import {models} from '@tonality/library-common';
import Uuid from '@tonality/library-uuid';
import {ALLOWED_DURATIONS, toRawDuration} from '@tonality/library-song-utils';
import { LeftSidebarWithMenu } from '@theoryofnekomata/viewfinder'
import styled from 'styled-components';
import MusicalKeyboard from '@theoryofnekomata/react-musical-keyboard';
@@ -8,7 +10,6 @@ import TextArea from '../../../molecules/forms/TextArea';
import ActionButton from '../../../molecules/forms/ActionButton';
import ToggleButton from '../../../molecules/forms/ToggleButton';
import NumericInput from '../../../molecules/forms/NumericInput';
import {ALLOWED_DURATIONS, toRawDuration} from '@tonality/library-song-utils';
import Timer from '../../../molecules/timer/Timer';

const KeyboardContainer = styled(LeftSidebarWithMenu.ContentContainer)({
@@ -150,20 +151,25 @@ const CreateRingtoneForm: FC<Props> = ({
<FormContents>

{
typeof (defaultValues.id as unknown) === 'string'
defaultValues.id
&& (
<input
type="hidden"
name="id"
defaultValue={defaultValues.id}
defaultValue={Uuid.parse(defaultValues.id.toString()).toString()}
/>
)
}
{
defaultValues.composerUserId
&& (
<input
type="hidden"
name="composerId"
defaultValue={Uuid.parse(defaultValues.composerUserId.toString()).toString()}
/>
)
}
<input
type="hidden"
name="composerId"
defaultValue={defaultValues.composerId}
/>
<Primary>
<TextInput
label={labels['name'] || 'Name'}
@@ -218,7 +224,7 @@ const CreateRingtoneForm: FC<Props> = ({
<ActionButton
onClick={typeof togglePlayback === 'function' ? togglePlayback({ formRef, setPlayTimestamp, setPlaying, }) : undefined}
>
{playing ? '⏹' : '▶️'}
{playing ? '■' : '▶'}
</ActionButton>
</OtherTools>
</Toolbar>


+ 21
- 5
packages/app-web/src/components/templates/CreateRingtone/index.tsx Näytä tiedosto

@@ -19,10 +19,18 @@ const Padding = styled('div')({
padding: '2rem 0',
})

const Avatar = styled('img')({
objectFit: 'cover',
objectPosition: 'center',
width: '3rem',
height: '3rem',
borderRadius: '50%',
})

type Props = {
onSearch?: FormEventHandler,
onSubmit?: FormEventHandler,
composer: models.Composer,
composer: any,
currentRingtone?: models.Ringtone,
composerRingtones: models.Ringtone[],

@@ -55,9 +63,17 @@ const CreateRingtoneTemplate: FC<Props> = ({
<Brand />
}
userLink={
<div>
User
</div>
<Link
href={{
query: {
popup: 'user',
},
}}
>
<Avatar
src={composer.picture}
/>
</Link>
}
topBarComponent={TopBarComponent}
sidebarMenuComponent={SidebarMenuComponent}
@@ -125,7 +141,7 @@ const CreateRingtoneTemplate: FC<Props> = ({
onSubmit={onSubmit}
defaultValues={{
...currentRingtone,
composerId: composer.id,
composerUserId: composer.id,
}}
action="/api/a/create/ringtone"
labels={{


+ 6
- 1
packages/app-web/src/modules/ringtone/client.ts Näytä tiedosto

@@ -14,7 +14,12 @@ export default class RingtoneClient {

save = async (e: FormEvent & { submitter: HTMLInputElement | HTMLButtonElement }) => {
e.preventDefault()
const values = getFormValues(e.target as HTMLFormElement, { submitter: e.submitter })
const {tempo, name, data} = getFormValues(e.target as HTMLFormElement, { submitter: e.submitter })
const values = {
name,
data,
tempo: Number(tempo),
}
const response = await this.fetchClient(endpoints.create(values))
alert(response.statusText)
}


+ 3
- 0
packages/app-web/src/modules/ringtone/endpoints.ts Näytä tiedosto

@@ -1,12 +1,15 @@
import {models} from '@tonality/library-common';
import {FetchClientParams, Method} from '../../utils/api/fetch';

const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ijc3bnFZZjV1b0lRQ25xbld0UndaMyJ9.eyJpc3MiOiJodHRwczovL21vZGFsLmpwLmF1dGgwLmNvbS8iLCJzdWIiOiJYOE1PT2JKdk5QdG5lRDVMbzVJYjY0a1c1dVRWR2hVdUBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tb2RhbC5qcC5hdXRoMC5jb20vYXBpL3YyLyIsImlhdCI6MTYyMzA3Mzg5NywiZXhwIjoxNjIzMTYwMjk3LCJhenAiOiJYOE1PT2JKdk5QdG5lRDVMbzVJYjY0a1c1dVRWR2hVdSIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.WiRaFG375CbyHN3XIhf6joY3CYh89h7Xwt6vReWDOS7wKEMVRtXvd-VEBwxf_ibfIAdrB5qibjF5JwtCl18Kd8m2fEDVwj8z8qUKyChPipNezCGfbfqz6Kv_ykf06KEBwypsVwdk2YhAcxdspWuilMUPizAPkno8GXbjFYOpeZobwA4Y50zeKDWRP6SPCM94dlN7zf3myu98wBqOk4KiXH-cyO_dIVF42KTnnZlVfkEmJoLJUmSUUbRNPwrx6k-eQ2uP0whvwfhZRwo5u0uVxnnQBcEK0fTQ9CDJPKxbwULFkjfN0nLfxOLcRMdPFMNtcWEFcFDr6LHGnqDCEG4lEw'

export const create = (body: Partial<models.Ringtone>): FetchClientParams => ({
method: Method.POST,
url: ['', 'api', 'ringtones'].join('/'),
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
})



+ 3
- 1
packages/app-web/src/pages/my/create/ringtone/index.tsx Näytä tiedosto

@@ -1,6 +1,7 @@
import {GetServerSideProps, NextPage} from 'next'
import {useEffect, useState} from 'react';
import {models} from '@tonality/library-common'
import {useUser} from '@auth0/nextjs-auth0'
import CreateRingtoneTemplate from '../../../../components/templates/CreateRingtone'
import RingtoneClient from '../../../../modules/ringtone/client'
import {getSession, withPageAuthRequired} from '@auth0/nextjs-auth0';
@@ -9,7 +10,7 @@ import SoundManager from '../../../../utils/sound/SoundManager';
import ComposerClient from '../../../../modules/composer/client';

type Props = {
user: models.Composer,
user: models.User,
composerRingtones: models.Ringtone[],
}

@@ -20,6 +21,7 @@ const MyCreateRingtonePage: NextPage<Props> = ({
const [hydrated, setHydrated] = useState(false)
const [ringtoneClient, setRingtoneClient] = useState<RingtoneClient>(null)
const [composerClient, setComposerClient] = useState<ComposerClient>(null)
const theUser = useUser()

useEffect(() => {
setHydrated(true)


+ 2
- 2
packages/app-web/src/pages/ringtones/index.tsx Näytä tiedosto

@@ -4,7 +4,7 @@ import {models} from '@tonality/library-common'
import RingtoneClient from '../../modules/ringtone/client'

type Props = {
user: models.Composer,
user: models.User,
composerRingtones: models.Ringtone[],
}

@@ -32,7 +32,7 @@ const MyCreateRingtonePage: NextPage<Props> = ({

export const getServerSideProps: GetServerSideProps = async () => {
const user = {
id: '00000000-0000-0000-000000000000',
id: '00000000-0000-0000-0000-000000000000',
name: 'TheoryOfNekomata',
bio: '',
}


+ 1
- 1
packages/library-song-utils/src/index.ts Näytä tiedosto

@@ -1,4 +1,4 @@
import {Note, parseNote, PITCH_CLASSES} from "./note";
import {Note, parseNote, PITCH_CLASSES} from './note';
import {toRawDuration} from './duration';

export interface Playable {


+ 29
- 5
packages/library-uuid/src/index.ts Näytä tiedosto

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

function toString(this: Uuid) {
return stringify(this)
}

function toBuffer(this: Uuid) {
return Buffer.from(this)
}

const constructUuidBase = (bytes: any) => {
const uuidBase = Buffer.from(bytes) as Uuid
uuidBase.toString = toString.bind(uuidBase);
const uuidBaseExtend = uuidBase as unknown as Record<string, unknown>
uuidBaseExtend['toBuffer'] = toBuffer.bind(uuidBase)
return uuidBase
}

export interface UuidBuffer extends Buffer {
toBuffer?(): Buffer,
}

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

static from(bytes: any): UuidBuffer {
return constructUuidBase(bytes)
}

toString() {
toString(): string {
return stringify(this)
}

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

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

@@ -1,7 +1,5 @@
PORT=

DATABASE_URL=

DATABASE_USERNAME=

DATABASE_PASSWORD=


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

@@ -17,10 +17,11 @@
"license": "MIT",
"dependencies": {
"@abraham/reflection": "^0.8.0",
"@prisma/client": "^2.24.1",
"@prisma/client": "2.21.2",
"bcrypt": "^5.0.1",
"dotenv": "^9.0.2",
"fastify": "^3.0.0",
"fastify-auth0-verify": "^0.5.2",
"fastify-autoload": "^3.3.1",
"fastify-cli": "^2.11.0",
"fastify-cors": "^6.0.1",
@@ -28,6 +29,7 @@
"fastify-sensible": "^3.1.0",
"knex": "^0.95.5",
"tsyringe": "^4.5.0",
"unfetch": "^4.2.0",
"uuid": "^8.3.2"
},
"devDependencies": {
@@ -39,7 +41,7 @@
"cross-env": "^7.0.3",
"fastify-tsconfig": "^1.0.1",
"jest": "^26.6.3",
"prisma": "^2.24.1",
"prisma": "2.21.2",
"ts-jest": "^26.5.6",
"typescript": "^4.1.3"
},


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

@@ -16,7 +16,7 @@ model Ringtone {
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])
composer User @relation(fields: [composerUserId], references: [id])

@@map("ringtone")
}


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

@@ -16,6 +16,8 @@ const app: FastifyPluginAsync<AppOptions> = async (
// Place here your custom code!
const modules = await Promise.all([
import('./global'),
import('./modules/credentials'),
import('./modules/auth'),
import('./modules/password'),
import('./modules/user'),
import('./modules/ringtone'),
@@ -39,8 +41,6 @@ const app: FastifyPluginAsync<AppOptions> = async (
dir: join(__dirname, 'routes'),
options: opts,
});

};

export default app;
export {app};

+ 30
- 0
packages/service-core/src/modules/auth/controller.ts Näytä tiedosto

@@ -0,0 +1,30 @@
import {inject, singleton} from 'tsyringe';
import AuthService from './service';
import {Controller, ControllerResponse} from '../../utils/helpers';
import Credentials from '../credentials/type';

type AuthController = Controller<{
logIn: ControllerResponse<Credentials>,
}>

export default AuthController

@singleton()
export class AuthControllerImpl implements AuthController {
constructor(
@inject('AuthService')
private readonly authService: AuthService
) {}

logIn = async (request: any, reply: any) => {
try {
const data = await this.authService.logIn(request.body['username'], request.body['password'])
return {
data
}
} catch (err) {
reply.raw.statusMessage = 'Invalid Credentials'
reply.unauthorized()
}
}
}

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

@@ -0,0 +1,8 @@
import {DependencyContainer} from 'tsyringe';
import {AuthServiceImpl} from './service';
import {AuthControllerImpl} from './controller';

export default (container: DependencyContainer) => {
container.register('AuthController', { useClass: AuthControllerImpl })
container.register('AuthService', { useClass: AuthServiceImpl })
}

+ 37
- 0
packages/service-core/src/modules/auth/service.ts Näytä tiedosto

@@ -0,0 +1,37 @@
import {inject, singleton} from 'tsyringe';
import UserService from '../user/service';
import PasswordService from '../password/service';
import CredentialsService from '../credentials/service';
import Credentials from '../credentials/type';

export default interface AuthService {
logIn(username: string, password: string): Promise<Credentials>
}

@singleton()
export class AuthServiceImpl implements AuthService {
constructor(
@inject('UserService')
private readonly userService: UserService,
@inject('PasswordService')
private readonly passwordService: PasswordService,
@inject('CredentialsService')
private readonly credentialsService: CredentialsService,
) {}

async logIn(username: string, password: string): Promise<Credentials> {
const user = await this.userService.getByUsername(username)
if (!user) {
throw new Error('Invalid credentials.')
}

const valid = this.passwordService.compare(password, user.password)
if (!valid) {
throw new Error('Invalid credentials.')
}

const credentials = await this.credentialsService.request()
credentials.profile = await this.userService.getProfileByUsername(username)
return credentials
}
}

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

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

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

+ 34
- 0
packages/service-core/src/modules/credentials/service.ts Näytä tiedosto

@@ -0,0 +1,34 @@
import {URL} from 'url';
import unfetch from 'unfetch';
import Credentials from './type';
import {singleton} from 'tsyringe';

export default interface CredentialsService {
request(): Promise<Credentials>
}

@singleton()
export class CredentialsServiceImpl {
async request(): Promise<Credentials> {
const tokenUrl = new URL('/oauth/token', process.env.AUTH0_DOMAIN)
const audienceUrl = new URL('/api/v2', process.env.AUTH0_DOMAIN)
const response = await unfetch(tokenUrl.toString(), {
method: 'POST',
body: JSON.stringify({
client_id: process.env.AUTH0_CLIENT_ID,
client_secret: process.env.AUTH0_SECRET,
audience: audienceUrl.toString(),
grant_type: 'client_credentials',
})
})
if (!response.ok) {
throw new Error('Unable to request credentials.')
}
const { access_token: accessToken, expires_in: expiresIn, token_type: tokenType } = await response.json()
return {
accessToken,
expiresIn,
tokenType,
}
}
}

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

@@ -0,0 +1,6 @@
export default interface Credentials {
[k: string]: string | number | unknown,
accessToken: string,
expiresIn: number,
tokenType: string,
}

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

@@ -18,6 +18,12 @@ type RingtoneController = Controller<{

export default RingtoneController

const serializeRingtone = d => ({
...d,
composerUserId: d.composerUserId.toString(),
id: d.id.toString(),
})

@singleton()
export class RingtoneControllerImpl implements RingtoneController {
constructor(
@@ -28,7 +34,8 @@ export class RingtoneControllerImpl implements RingtoneController {
get = async (request: any, reply: any) => {
try {
const id = Uuid.parse(request.params['id'])
const data = await this.ringtoneService.get(id)
const rawData = await this.ringtoneService.get(id)
const data = serializeRingtone(rawData)
if (typeof (data.deletedAt as Date) !== 'undefined') {
reply.raw.statusMessage = 'Ringtone Deleted Previously'
reply.gone()
@@ -58,10 +65,12 @@ export class RingtoneControllerImpl implements RingtoneController {

const skip = !isNaN(skipNumber) ? skipNumber : 0
const take = !isNaN(takeNumber) ? takeNumber : 10
const rawData = await this.ringtoneService.browse(skip, take)
const data = rawData.map(serializeRingtone)

reply.raw.statusMessage = 'Multiple Ringtones Retrieved'
return {
data: await this.ringtoneService.browse(skip, take),
data,
skip,
take,
}
@@ -74,9 +83,12 @@ export class RingtoneControllerImpl implements RingtoneController {
search = async (request: any, reply: any) => {
try {
const { 'q': query } = request.query
const rawData = await this.ringtoneService.search(query)
const data = rawData.map(serializeRingtone)

reply.raw.statusMessage = 'Search Results Retrieved'
return {
data: await this.ringtoneService.search(query),
data,
}
} catch (err) {
reply.raw.statusMessage = 'Search Error'
@@ -85,12 +97,15 @@ export class RingtoneControllerImpl implements RingtoneController {
}

create = async (request: any, reply: any) => {
// TODO parse and validate body

try {
const { composerUserId, ...etcBody } = request.body
const data = await this.ringtoneService.create({
...etcBody,
composerUserId: Uuid.parse(composerUserId),
const rawData = await this.ringtoneService.create({
...request.body,
// TODO map auth credentials to user
composerUserId: Uuid.parse(request.body.composerUserId),
})
const data = serializeRingtone(rawData)
reply.raw.statusMessage = 'Ringtone Created'
reply.raw.statusCode = 201
return {
@@ -106,10 +121,11 @@ export class RingtoneControllerImpl implements RingtoneController {
try {
// TODO validate data
const id = Uuid.parse(request.params['id'])
const data = await this.ringtoneService.update({
const rawData = await this.ringtoneService.update({
...request.body,
id,
})
const data = serializeRingtone(rawData)
if (data.deletedAt) {
reply.raw.statusMessage = 'Ringtone Deleted Previously'
reply.gone()
@@ -133,7 +149,8 @@ export class RingtoneControllerImpl implements RingtoneController {
try {
// TODO validate data
const id = Uuid.parse(request.params['id'])
const data = await this.ringtoneService.softDelete(id)
const rawData = await this.ringtoneService.softDelete(id)
const data = serializeRingtone(rawData)
if (!data) {
reply.raw.statusMessage = 'Ringtone Not Found'
reply.notFound()
@@ -153,7 +170,8 @@ export class RingtoneControllerImpl implements RingtoneController {
try {
// TODO validate data
const id = Uuid.parse(request.params['id'])
const data = await this.ringtoneService.undoDelete(id)
const rawData = await this.ringtoneService.undoDelete(id)
const data = serializeRingtone(rawData)
if (!data) {
reply.raw.statusMessage = 'Ringtone Not Found'
reply.notFound()


+ 43
- 11
packages/service-core/src/modules/ringtone/service.ts Näytä tiedosto

@@ -1,12 +1,13 @@
import {inject, singleton} from 'tsyringe'
import {models} from '@tonality/library-common'
import Uuid from '@tonality/library-uuid'
import Uuid, {UuidBuffer} from '@tonality/library-uuid';
import {PrismaClient} from '@prisma/client'

export default interface RingtoneService {
get(id: Uuid): Promise<models.Ringtone>
browseByComposer(composerUserId: Uuid, skip?: number, take?: number): Promise<models.Ringtone[]>
browse(skip?: number, take?: number): Promise<models.Ringtone[]>
search(q?: string): 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>
@@ -14,6 +15,15 @@ export default interface RingtoneService {
hardDelete(id: Uuid): Promise<void>
}

const serializeRingtone = d => {
return ({
...d,
composerUserId: Uuid.from(d.composerUserId).toString(),
id: Uuid.from(d.id).toString(),
})
}


@singleton()
export class RingtoneServiceImpl {
constructor(
@@ -21,6 +31,22 @@ export class RingtoneServiceImpl {
private readonly prismaClient: PrismaClient,
) {}

async browseByComposer(composerUserId: Uuid, skip: number = 0, take: number = 10): Promise<models.Ringtone[]> {
const rawData = await this.prismaClient.ringtone.findMany({
where: {
composer: {
id: {
equals: composerUserId,
}
}
},
skip,
take,
})

return rawData.map(serializeRingtone)
}

async get(id: Uuid): Promise<models.Ringtone> {
const ringtone = await this.prismaClient.ringtone.findUnique({
where: {
@@ -32,14 +58,16 @@ export class RingtoneServiceImpl {
throw new Error('Ringtone not found!')
}

return ringtone
return serializeRingtone(ringtone)
}

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

return rawData.map(serializeRingtone)
}

async search(q: string = ''): Promise<models.Ringtone[]> {
@@ -54,31 +82,33 @@ export class RingtoneServiceImpl {

async create(data: Partial<models.Ringtone>): Promise<models.Ringtone> {
const { createdAt, updatedAt, deletedAt, composerUserId, name, ...safeData } = data
return this.prismaClient.ringtone.create({
const rawData = await this.prismaClient.ringtone.create({
data: {
...safeData,
id: Uuid.new(),
composerUserId: composerUserId as Uuid,
id: Uuid.new().toBuffer(),
composerUserId: (composerUserId as UuidBuffer).toBuffer(),
name: name as string,
},
})
return serializeRingtone(rawData)
}

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

async softDelete(id: Uuid): Promise<models.Ringtone> {
return this.prismaClient.ringtone.update({
const rawData = await this.prismaClient.ringtone.update({
where: {
id,
},
@@ -86,10 +116,11 @@ export class RingtoneServiceImpl {
deletedAt: new Date(),
},
})
return serializeRingtone(rawData)
}

async undoDelete(id: Uuid): Promise<models.Ringtone> {
return this.prismaClient.ringtone.update({
const rawData = this.prismaClient.ringtone.update({
where: {
id,
},
@@ -97,6 +128,7 @@ export class RingtoneServiceImpl {
deletedAt: null,
},
})
return serializeRingtone(rawData)
}

async hardDelete(id: Uuid): Promise<void> {


+ 17
- 2
packages/service-core/src/modules/user/service.ts Näytä tiedosto

@@ -6,7 +6,8 @@ import PasswordService from '../password/service'

export default interface UserService {
get(id: Uuid): Promise<models.UserProfile>
getByUsername(username: string): Promise<models.UserProfile>
getByUsername(username: string): Promise<models.User>
getProfileByUsername(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>
@@ -38,7 +39,21 @@ export class UserServiceImpl implements UserService {
return user
}

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

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

return user
}

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


+ 9
- 0
packages/service-core/src/plugins/auth0-verify.ts Näytä tiedosto

@@ -0,0 +1,9 @@
import FastifyAuth0Verify, { FastifyAuth0VerifyOptions } from 'fastify-auth0-verify';
import fp from 'fastify-plugin';

export default fp<FastifyAuth0VerifyOptions>(async (fastify, opts) => {
fastify.register(FastifyAuth0Verify, {
domain: process.env.AUTH0_DOMAIN,
secret: process.env.AUTH0_SECRET
})
});

+ 14
- 0
packages/service-core/src/routes/api/auth/index.ts Näytä tiedosto

@@ -0,0 +1,14 @@
import {FastifyPluginAsync} from 'fastify'
import {container} from 'tsyringe'
import AuthController from '../../../modules/auth/controller'

const auth: FastifyPluginAsync = async (fastify): Promise<void> => {
const ringtoneController = container.resolve<AuthController>('AuthController')
fastify.route({
url: '/log-in',
method: 'POST',
handler: ringtoneController.logIn,
})
}

export default auth

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

@@ -4,13 +4,52 @@ import RingtoneController from '../../../modules/ringtone/controller'

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

fastify.route({
url: '/',
method: 'GET',
handler: ringtoneController.browse,
})

fastify.route({
url: '/',
method: 'POST',
handler: ringtoneController.create,
preValidation: fastify.authenticate,
})

fastify.route({
url: '/:id',
method: 'PATCH',
handler: ringtoneController.update,
preValidation: fastify.authenticate,
})

fastify.route({
url: '/:id/delete',
method: 'POST',
handler: ringtoneController.softDelete,
preValidation: fastify.authenticate,
})

fastify.route({
url: '/:id/delete',
method: 'DELETE',
handler: ringtoneController.undoDelete,
preValidation: fastify.authenticate,
})

fastify.route({
url: '/:id',
method: 'DELETE',
handler: ringtoneController.hardDelete,
preValidation: fastify.authenticate,
})
}

export default ringtones

+ 2
- 1
packages/service-core/tsconfig.json Näytä tiedosto

@@ -3,7 +3,8 @@
"compilerOptions": {
"outDir": "dist",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
"emitDecoratorMetadata": true,
"strict": false
},
"include": [
"src/**/*.ts"


+ 187
- 22
packages/service-core/yarn.lock Näytä tiedosto

@@ -515,22 +515,22 @@
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==
"@prisma/client@2.21.2":
version "2.21.2"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.21.2.tgz#ca8489832da1d61add429390210be4d7896e5e29"
integrity sha512-UjkOXYpxLuHyoMDsP2m0LTcxhrjQa1dEOLFe3aDrO/BLrs/2yUxyPdtwSKxizRXFzuXSGkKIK225vcjZRuMpAg==
dependencies:
"@prisma/engines-version" "2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4"
"@prisma/engines-version" "2.21.0-36.e421996c87d5f3c8f7eeadd502d4ad402c89464d"
"@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-version@2.21.0-36.e421996c87d5f3c8f7eeadd502d4ad402c89464d":
version "2.21.0-36.e421996c87d5f3c8f7eeadd502d4ad402c89464d"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.21.0-36.e421996c87d5f3c8f7eeadd502d4ad402c89464d.tgz#b749bae4173eb766dafc298aaa7d883c2dbe555b"
integrity sha512-9/fE1gdPWmjbMjXUJjrTMt848TsgEnSjZCcJ1wu9OAcRlAKKJBLehftqC3gSEShDijvMYgeTdGU5snMpwmv4vg==
"@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==
"@prisma/engines@2.21.0-36.e421996c87d5f3c8f7eeadd502d4ad402c89464d":
version "2.21.0-36.e421996c87d5f3c8f7eeadd502d4ad402c89464d"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.21.0-36.e421996c87d5f3c8f7eeadd502d4ad402c89464d.tgz#aafed60c9506bc766e49ea60b9f8ce7da2385bc6"
integrity sha512-L57tvSoom2GDWDqik4wrAUBvLTAv5MTm2OOzNMBKsv0w5cX7ONoZ8KnGQN+csmdJpQVBs93dIvIBm72OO+l/9Q==
"@sinonjs/commons@^1.7.0":
version "1.8.3"
@@ -620,6 +620,13 @@
jest-diff "^26.0.0"
pretty-format "^26.0.0"
"@types/jsonwebtoken@^8.5.0":
version "8.5.1"
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#56958cb2d80f6d74352bd2e501a018e2506a8a84"
integrity sha512-rNAPdomlIUX0i0cg2+I+Q1wOUr531zHBQ+cV/28PJ39bSPKjahatZZ2LMuhiguETkCgLVzfruw/ZvNMNkKoSzw==
dependencies:
"@types/node" "*"
"@types/node@*", "@types/node@^15.0.0":
version "15.0.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.3.tgz#ee09fcaac513576474c327da5818d421b98db88a"
@@ -1082,6 +1089,11 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
buffer-from@1.x, buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -1263,6 +1275,11 @@ cliui@^7.0.2:
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
clone@2.x:
version "2.1.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
close-with-grace@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/close-with-grace/-/close-with-grace-1.1.0.tgz#91a48cf2019b5ae6e67b0255a32abcfd9bbca233"
@@ -1606,6 +1623,13 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
ecdsa-sig-formatter@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
electron-to-chromium@^1.3.723:
version "1.3.727"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz#857e310ca00f0b75da4e1db6ff0e073cc4a91ddf"
@@ -1841,6 +1865,24 @@ fast-safe-stringify@^2.0.7:
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
fastfall@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/fastfall/-/fastfall-1.5.1.tgz#3fee03331a49d1d39b3cdf7a5e9cd66f475e7b94"
integrity sha1-P+4DMxpJ0dObPN96XpzWb0dee5Q=
dependencies:
reusify "^1.0.0"
fastify-auth0-verify@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fastify-auth0-verify/-/fastify-auth0-verify-0.5.2.tgz#26d7e51d9f251d515f3edde97669ac21ecd0aa67"
integrity sha512-MHNolhH4BRRQEzUWHdSfa03/Aeas/NYvjaRTopF17ovta55ErWJa3e6UpxmXKjMArFP/NeyZkcZ3ZBci71Fz1Q==
dependencies:
fastify-jwt "^3.0.0"
fastify-plugin "^3.0.0"
http-errors "^1.7.3"
node-cache "^5.0.1"
node-fetch "^2.6.1"
fastify-autoload@^3.3.1:
version "3.7.1"
resolved "https://registry.yarnpkg.com/fastify-autoload/-/fastify-autoload-3.7.1.tgz#64dd843c5fe340d4c42d3d9353521e70c4c28b4f"
@@ -1887,6 +1929,17 @@ fastify-error@^0.3.0:
resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.3.1.tgz#8eb993e15e3cf57f0357fc452af9290f1c1278d2"
integrity sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==
fastify-jwt@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fastify-jwt/-/fastify-jwt-3.0.0.tgz#d0cf2bfc02cf12b3826f534258f559baf9c8518f"
integrity sha512-gFo6AQRz71TEteKisNcgXNYei1QzNBzXH59J00PBoRHhK3YtADF/teQxrKTyR2GeIBwuQnVJNRtsFRgLd7adTA==
dependencies:
"@types/jsonwebtoken" "^8.5.0"
fastify-plugin "^3.0.0"
http-errors "^1.8.0"
jsonwebtoken "^8.5.1"
steed "^1.1.3"
fastify-plugin@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-3.0.0.tgz#cf1b8c8098e3b5a7c8c30e6aeb06903370c054ca"
@@ -1936,13 +1989,29 @@ fastify@^3.0.0:
semver "^7.3.2"
tiny-lru "^7.0.0"
fastq@^1.6.1:
fastparallel@^2.2.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/fastparallel/-/fastparallel-2.4.0.tgz#65fbec1a5e5902494be772cf5765cbaaece08688"
integrity sha512-sacwQ7wwKlQXsa7TN24UvMBLZNLmVcPhmxccC9riFqb3N+fSczJL8eWdnZodZ/KijGVgNBBfvF/NeXER08uXnQ==
dependencies:
reusify "^1.0.4"
xtend "^4.0.2"
fastq@^1.3.0, fastq@^1.6.1:
version "1.11.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858"
integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==
dependencies:
reusify "^1.0.4"
fastseries@^1.7.0:
version "1.7.2"
resolved "https://registry.yarnpkg.com/fastseries/-/fastseries-1.7.2.tgz#d22ce13b9433dff3388d91dbd6b8bda9b21a0f4b"
integrity sha1-0izhO5Qz3/M4jZHb1ri9qbIaD0s=
dependencies:
reusify "^1.0.0"
xtend "^4.0.0"
fb-watchman@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
@@ -2279,7 +2348,7 @@ html-escaper@^2.0.0:
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
http-errors@^1.7.3:
http-errors@^1.7.3, http-errors@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507"
integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==
@@ -3079,6 +3148,22 @@ json5@2.x, json5@^2.1.2:
dependencies:
minimist "^1.2.5"
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
dependencies:
jws "^3.2.2"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
semver "^5.6.0"
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -3089,6 +3174,23 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.1"
safe-buffer "^5.0.1"
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -3186,6 +3288,41 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash.toarray@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
@@ -3453,6 +3590,13 @@ node-addon-api@^3.1.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
node-cache@^5.0.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d"
integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==
dependencies:
clone "2.x"
node-emoji@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da"
@@ -3943,12 +4087,12 @@ 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==
prisma@2.21.2:
version "2.21.2"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.21.2.tgz#a73b4cbe92a884aa98b317684d6741871b5e94a5"
integrity sha512-Ux9ovDIUHsMNLGLtuo6BBKCuuBVLpZmhM2LXF+VBUQvsbmsVfp3u5CRyHGEqaZqMibYQJISy7YZYF/RgozHKkQ==
dependencies:
"@prisma/engines" "2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4"
"@prisma/engines" "2.21.0-36.e421996c87d5f3c8f7eeadd502d4ad402c89464d"
process-nextick-args@~2.0.0:
version "2.0.1"
@@ -4193,7 +4337,7 @@ ret@~0.2.0:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c"
integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==
reusify@^1.0.4:
reusify@^1.0.0, reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
@@ -4295,7 +4439,7 @@ semver-store@^0.3.0:
resolved "https://registry.yarnpkg.com/semver-store/-/semver-store-0.3.0.tgz#ce602ff07df37080ec9f4fb40b29576547befbe9"
integrity sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0:
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -4571,6 +4715,17 @@ stealthy-require@^1.1.1:
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
steed@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/steed/-/steed-1.1.3.tgz#f1525dd5adb12eb21bf74749537668d625b9abc5"
integrity sha1-8VJd1a2xLrIb90dJU3Zo1iW5q8U=
dependencies:
fastfall "^1.5.0"
fastparallel "^2.2.0"
fastq "^1.3.0"
fastseries "^1.7.0"
reusify "^1.0.0"
string-length@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
@@ -4948,6 +5103,11 @@ typescript@^4.1.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961"
integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==
unfetch@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==
union-value@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
@@ -5188,6 +5348,11 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xtend@^4.0.0, xtend@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
y18n@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"


Ladataan…
Peruuta
Tallenna