Compare commits
10
Commits
c0d6834178
...
2c84ea0f25
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c84ea0f25
|
||
|
|
2224c8f3bb
|
||
|
|
90c048a282
|
||
|
|
1d54d287e1
|
||
|
|
3b9b28d326
|
||
|
|
4ee7ed3e5e
|
||
|
|
0afa9251d6
|
||
|
|
250ae822ea
|
||
|
|
b36f53f022
|
||
|
|
e4187a697a
|
@@ -1,17 +1,13 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"extends": "eslint:recommended",
|
||||
"env": {
|
||||
"commonjs": true,
|
||||
"es6": true,
|
||||
"es2022": true,
|
||||
"node": true,
|
||||
"browser": false
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": false
|
||||
},
|
||||
"ecmaVersion": 2022,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"globals": {
|
||||
|
||||
@@ -20,6 +20,9 @@ jobs:
|
||||
- name: Installer les dépendances
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Lancer les tests
|
||||
run: yarn test
|
||||
|
||||
- name: Build Strapi
|
||||
run: yarn build
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ jobs:
|
||||
- name: Installer les dépendances
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Lancer les tests
|
||||
run: yarn test
|
||||
|
||||
- name: Build Strapi
|
||||
run: yarn build
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"collectionName": "components_store_stores",
|
||||
"info": {
|
||||
"name": "Album",
|
||||
"icon": "music",
|
||||
"description": ""
|
||||
},
|
||||
"options": {},
|
||||
"attributes": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"boutik": {
|
||||
"type": "enumeration",
|
||||
"enum": [
|
||||
"Tidal",
|
||||
"Spotify",
|
||||
"Deezer",
|
||||
"Qobuz",
|
||||
"Youtubemusic",
|
||||
"Applemusic",
|
||||
"Amazon",
|
||||
"Soundcloud"
|
||||
],
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"collectionName": "components_trad_traductions",
|
||||
"info": {
|
||||
"name": "traductions",
|
||||
"icon": "spell-check",
|
||||
"description": ""
|
||||
},
|
||||
"options": {},
|
||||
"attributes": {
|
||||
"francais": {
|
||||
"type": "richtext"
|
||||
},
|
||||
"english": {
|
||||
"type": "richtext"
|
||||
},
|
||||
"espagnol": {
|
||||
"type": "richtext"
|
||||
},
|
||||
"deutsch": {
|
||||
"type": "richtext"
|
||||
},
|
||||
"italiano": {
|
||||
"type": "richtext"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"collectionName": "components_url_liens",
|
||||
"info": {
|
||||
"name": "liens",
|
||||
"icon": "link",
|
||||
"description": ""
|
||||
},
|
||||
"options": {},
|
||||
"attributes": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"sit": {
|
||||
"type": "enumeration",
|
||||
"enum": [
|
||||
"Youtube",
|
||||
"Peertube",
|
||||
"Dailymotion",
|
||||
"Vimeo",
|
||||
"File",
|
||||
"Lbry",
|
||||
"Rumble"
|
||||
],
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -4,10 +4,10 @@ const forgotPasswordTemplate = require('./email-templates/forgot-password');
|
||||
module.exports = ({ env }) => ({
|
||||
url: env('STRAPI_ADMIN_URL', '/admin'),
|
||||
apiToken: {
|
||||
salt: env('API_TOKEN_SALT', 'd9b0df66ff97a666027e665707b4e3e7'),
|
||||
salt: env('API_TOKEN_SALT'),
|
||||
},
|
||||
auth: {
|
||||
secret: env('ADMIN_JWT_SECRET', '77b2c87dbab4e1697bec244226fbd1b3'),
|
||||
secret: env('ADMIN_JWT_SECRET'),
|
||||
},
|
||||
forgotPassword: {
|
||||
from: env('SMTP_FROM'),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const subject = `Réinitialiser le mot de passe`;
|
||||
const subject = 'Réinitialiser le mot de passe';
|
||||
|
||||
const html = `<p>Bèl bonjou <%= user.firstname %></p>
|
||||
<p>Nous avons appris que tu a perdu ton mot de passe. Nous en sommes désolés ! </p>
|
||||
|
||||
+1
-1
@@ -16,4 +16,4 @@ module.exports = ({env}) => ({
|
||||
defaultReplyTo: env('SMTP_REPLY_TO'),
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const cronTasks = require("./cron-task");
|
||||
const cronTasks = require('./cron-task');
|
||||
|
||||
module.exports = ({ env }) => ({
|
||||
host: env('HOST', '0.0.0.0'),
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"title": "#OKi API Dokiman",
|
||||
"description": "",
|
||||
"termsOfService": null,
|
||||
"contact": {
|
||||
"name": "#OKi",
|
||||
"email": "kontak@o-k-i.net",
|
||||
"url": "https://o-k-i.net"
|
||||
},
|
||||
"license": null
|
||||
},
|
||||
"x-strapi-config": {
|
||||
"path": "/dokiman",
|
||||
"showGeneratedFiles": true,
|
||||
"pluginsForWhichToGenerateDoc": []
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.o-k-i.net",
|
||||
"description": "Production server"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:1337",
|
||||
"description": "Development server"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:1337",
|
||||
"description": "Staging server"
|
||||
}
|
||||
],
|
||||
"externalDocs": null,
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"paths": {},
|
||||
"tags": [],
|
||||
"components": {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,136 +0,0 @@
|
||||
<!DOCTYPE html><html><head>
|
||||
<title>Login - Documentation</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Lato:400,700" rel="stylesheet">
|
||||
<style>
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background-color: #ffffff;
|
||||
font-family: 'Lato';
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.login {
|
||||
height: 100%;
|
||||
background-color: #F6F9FC;
|
||||
}
|
||||
|
||||
.login .login-form {
|
||||
height: calc(100% - 70px);
|
||||
padding: 68px 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login .login-form form {
|
||||
position: relative;
|
||||
max-width: 460px;
|
||||
padding: 26px 30px;
|
||||
margin: 55px auto 0;
|
||||
background-color: #ffffff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0px 2px 4px rgba(91, 107, 174, .15);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login .login-form form:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 0px;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #2B66CC;
|
||||
}
|
||||
|
||||
.login .login-form form .error {
|
||||
display: block;
|
||||
color: #FF4E00;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.login .login-form .sub-title {
|
||||
margin-top: 35px;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.login .login-form .logo{
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
.login .login-form form label {
|
||||
display: block;
|
||||
margin-bottom: 18px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login .login-form form input {
|
||||
outline: none;
|
||||
width: calc(100% - 30px);
|
||||
height: 36px;
|
||||
padding: 0 15px;
|
||||
border: 1px solid #ECECEC;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 20px;
|
||||
line-height: 36px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.login .login-form form input[type=submit] {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
margin: 12px auto 0;
|
||||
padding: 0 75px;
|
||||
background: transparent;
|
||||
border-radius: 36px;
|
||||
border: 1px solid #2B66CC;
|
||||
color: #2B66CC;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
transition: all .2s ease-out;
|
||||
}
|
||||
|
||||
.login .login-form form input[type=submit]:hover {
|
||||
background: #2B66CC;
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login">
|
||||
<section class="login-form">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-lg-offset-3 col-md-12">
|
||||
<img alt="Strapi logo" class="logo" src="https://strapi.io/assets/images/logo_login.png">
|
||||
<h2 class="sub-title">Enter the password to access the documentation.</h2>
|
||||
<form method="post" action="/dokiman/login">
|
||||
<span class="error">Wrong password...</span>
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" placeholder="•••••••••">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
</body></html>
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
jwtSecret: process.env.JWT_SECRET || '3527426b-d513-44a5-b4c8-ee16494bab0d'
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
{
|
||||
"kind": "collectionType",
|
||||
"collectionName": "users-permissions_user",
|
||||
"info": {
|
||||
"name": "user",
|
||||
"description": ""
|
||||
},
|
||||
"options": {
|
||||
"draftAndPublish": false,
|
||||
"timestamps": true,
|
||||
"privateAttributes": [
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"username": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"unique": true,
|
||||
"configurable": false,
|
||||
"required": true
|
||||
},
|
||||
"email": {
|
||||
"type": "email",
|
||||
"minLength": 6,
|
||||
"configurable": false,
|
||||
"required": true,
|
||||
"private": true
|
||||
},
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"configurable": false,
|
||||
"private": true
|
||||
},
|
||||
"password": {
|
||||
"type": "password",
|
||||
"minLength": 6,
|
||||
"configurable": false,
|
||||
"private": true
|
||||
},
|
||||
"resetPasswordToken": {
|
||||
"type": "string",
|
||||
"configurable": false,
|
||||
"private": true
|
||||
},
|
||||
"confirmationToken": {
|
||||
"type": "string",
|
||||
"configurable": false,
|
||||
"private": true
|
||||
},
|
||||
"confirmed": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"configurable": false,
|
||||
"private": true
|
||||
},
|
||||
"blocked": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"configurable": false,
|
||||
"private": true
|
||||
},
|
||||
"role": {
|
||||
"model": "role",
|
||||
"via": "users",
|
||||
"plugin": "users-permissions",
|
||||
"configurable": false,
|
||||
"private": true
|
||||
},
|
||||
"canAutoTranslate": {
|
||||
"type": "boolean",
|
||||
"private": false
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -18,9 +18,11 @@
|
||||
"start": "strapi start",
|
||||
"build": "strapi build",
|
||||
"strapi": "strapi",
|
||||
"test": "vitest run"
|
||||
"test": "vitest run",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.7.0",
|
||||
"vitest": "^4.1.9"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
+21
-21
@@ -15,14 +15,14 @@ export default {
|
||||
fr: {
|
||||
'Auth.form.welcome.subtitle': `Connectez-vous à votre compte ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API`,
|
||||
'Auth.form.welcome.title': `Bienvenue sur ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`,
|
||||
'LeftMenu.navbrand.title': 'Tableau de bord',
|
||||
'LeftMenu.navbrand.workplace': 'Menu',
|
||||
'LeftMenu.navbrand.title': 'Tableau de bord',
|
||||
'LeftMenu.navbrand.workplace': 'Menu',
|
||||
},
|
||||
en: {
|
||||
'Auth.form.welcome.subtitle': `Log in to your ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API account`,
|
||||
'Auth.form.welcome.title': `Welcome to ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`,
|
||||
'LeftMenu.navbrand.title': 'Dashboard',
|
||||
'LeftMenu.navbrand.workplace': 'Workplace',
|
||||
'Auth.form.welcome.subtitle': `Log in to your ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API account`,
|
||||
'Auth.form.welcome.title': `Welcome to ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`,
|
||||
'LeftMenu.navbrand.title': 'Dashboard',
|
||||
'LeftMenu.navbrand.workplace': 'Workplace',
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
@@ -30,9 +30,9 @@ export default {
|
||||
},
|
||||
theme: {
|
||||
colors: {
|
||||
buttonNeutral0: '#ffffff',
|
||||
buttonPrimary500: '#4caf50',
|
||||
buttonPrimary600: '#43a047',
|
||||
buttonNeutral0: '#ffffff',
|
||||
buttonPrimary500: '#4caf50',
|
||||
buttonPrimary600: '#43a047',
|
||||
primary100: '#c8e6c9',
|
||||
primary200: '#a5d6a7',
|
||||
primary500: '#4caf50',
|
||||
@@ -54,18 +54,18 @@ export default {
|
||||
succes600: '#7cb342',
|
||||
succes700: '#689f38',
|
||||
danger700: '#d32f2f',
|
||||
neutral0: '#ffffff',
|
||||
neutral100: '#f6f6f9',
|
||||
neutral1000: '#181826',
|
||||
neutral150: '#eaeaef',
|
||||
neutral200: '#dcdce4',
|
||||
neutral300: '#c0c0cf',
|
||||
neutral400: '#a5a5ba',
|
||||
neutral500: '#8e8ea9',
|
||||
neutral600: '#666687',
|
||||
neutral700: '#4a4a6a',
|
||||
neutral800: '#32324d',
|
||||
neutral900: '#212134'
|
||||
neutral0: '#ffffff',
|
||||
neutral100: '#f6f6f9',
|
||||
neutral1000: '#181826',
|
||||
neutral150: '#eaeaef',
|
||||
neutral200: '#dcdce4',
|
||||
neutral300: '#c0c0cf',
|
||||
neutral400: '#a5a5ba',
|
||||
neutral500: '#8e8ea9',
|
||||
neutral600: '#666687',
|
||||
neutral700: '#4a4a6a',
|
||||
neutral800: '#32324d',
|
||||
neutral900: '#212134'
|
||||
},
|
||||
},
|
||||
tutorials: false,
|
||||
|
||||
@@ -1,66 +1,66 @@
|
||||
import {describe, it, expect, vi, afterEach} from 'vitest'
|
||||
import {describe, it, expect, vi, afterEach} from 'vitest';
|
||||
|
||||
async function loadLifecycles(strapiMock) {
|
||||
vi.resetModules()
|
||||
global.strapi = strapiMock
|
||||
const mod = await import('../lifecycles.js')
|
||||
return mod
|
||||
vi.resetModules();
|
||||
global.strapi = strapiMock;
|
||||
const mod = await import('../lifecycles.js');
|
||||
return mod;
|
||||
}
|
||||
|
||||
describe('artiste afterUpdate — cascade de renommage des slugs', () => {
|
||||
afterEach(() => {
|
||||
delete global.strapi
|
||||
})
|
||||
delete global.strapi;
|
||||
});
|
||||
|
||||
it("renomme le slug des paroles de l'artiste quand son alias change", async () => {
|
||||
it('renomme le slug des paroles de l\'artiste quand son alias change', async () => {
|
||||
const artisteFindOne = vi.fn(async () => ({
|
||||
id: 5,
|
||||
paroles: [{id: 10}, {id: 11}]
|
||||
}))
|
||||
}));
|
||||
const paroleFindMany = vi.fn(async () => [
|
||||
{id: 10, titre: 'Titre 1', slug: 'ancien-alias-titre-1', artistes: [{alias: 'nouvel-alias'}]},
|
||||
{id: 11, titre: 'Titre 2', slug: 'nouvel-alias-titre-2', artistes: [{alias: 'nouvel-alias'}]}
|
||||
])
|
||||
const paroleUpdate = vi.fn(async () => {})
|
||||
]);
|
||||
const paroleUpdate = vi.fn(async () => {});
|
||||
|
||||
const strapiMock = {
|
||||
db: {
|
||||
query: vi.fn(uid => {
|
||||
if (uid === 'api::artiste.artiste') return {findOne: artisteFindOne}
|
||||
if (uid === 'api::parole.parole') return {findMany: paroleFindMany, update: paroleUpdate}
|
||||
throw new Error(`unexpected uid: ${uid}`)
|
||||
if (uid === 'api::artiste.artiste') return {findOne: artisteFindOne};
|
||||
if (uid === 'api::parole.parole') return {findMany: paroleFindMany, update: paroleUpdate};
|
||||
throw new Error(`unexpected uid: ${uid}`);
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const {afterUpdate} = await loadLifecycles(strapiMock)
|
||||
const {afterUpdate} = await loadLifecycles(strapiMock);
|
||||
|
||||
await afterUpdate({result: {id: 5}})
|
||||
await afterUpdate({result: {id: 5}});
|
||||
|
||||
expect(artisteFindOne).toHaveBeenCalledWith({where: {id: 5}, populate: ['paroles']})
|
||||
expect(paroleFindMany).toHaveBeenCalledWith({where: {id: {$in: [10, 11]}}, populate: ['artistes']})
|
||||
expect(paroleUpdate).toHaveBeenCalledTimes(1)
|
||||
expect(paroleUpdate).toHaveBeenCalledWith({where: {id: 10}, data: {slug: 'nouvel-alias-titre-1'}})
|
||||
})
|
||||
expect(artisteFindOne).toHaveBeenCalledWith({where: {id: 5}, populate: ['paroles']});
|
||||
expect(paroleFindMany).toHaveBeenCalledWith({where: {id: {$in: [10, 11]}}, populate: ['artistes']});
|
||||
expect(paroleUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(paroleUpdate).toHaveBeenCalledWith({where: {id: 10}, data: {slug: 'nouvel-alias-titre-1'}});
|
||||
});
|
||||
|
||||
it("ne fait rien quand l'artiste n'a aucune parole", async () => {
|
||||
const artisteFindOne = vi.fn(async () => ({id: 5, paroles: []}))
|
||||
const paroleFindMany = vi.fn()
|
||||
it('ne fait rien quand l\'artiste n\'a aucune parole', async () => {
|
||||
const artisteFindOne = vi.fn(async () => ({id: 5, paroles: []}));
|
||||
const paroleFindMany = vi.fn();
|
||||
|
||||
const strapiMock = {
|
||||
db: {
|
||||
query: vi.fn(uid => {
|
||||
if (uid === 'api::artiste.artiste') return {findOne: artisteFindOne}
|
||||
if (uid === 'api::parole.parole') return {findMany: paroleFindMany}
|
||||
throw new Error(`unexpected uid: ${uid}`)
|
||||
if (uid === 'api::artiste.artiste') return {findOne: artisteFindOne};
|
||||
if (uid === 'api::parole.parole') return {findMany: paroleFindMany};
|
||||
throw new Error(`unexpected uid: ${uid}`);
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const {afterUpdate} = await loadLifecycles(strapiMock)
|
||||
const {afterUpdate} = await loadLifecycles(strapiMock);
|
||||
|
||||
await afterUpdate({result: {id: 5}})
|
||||
await afterUpdate({result: {id: 5}});
|
||||
|
||||
expect(paroleFindMany).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
expect(paroleFindMany).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
'use strict';
|
||||
|
||||
const { ApplicationError } = require("@strapi/utils").errors
|
||||
const slugify = require('slugify')
|
||||
const { ApplicationError } = require('@strapi/utils').errors;
|
||||
const slugify = require('slugify');
|
||||
|
||||
const jwennTeksEpiId = async ids => {
|
||||
const paroles = await strapi.db.query('api::parole.parole').findMany({
|
||||
where: {id: {$in: ids}},
|
||||
populate: ['artistes']
|
||||
})
|
||||
return paroles
|
||||
}
|
||||
});
|
||||
return paroles;
|
||||
};
|
||||
|
||||
const jwennAwtisEpiId = async id => {
|
||||
const artiste = await strapi.db.query('api::artiste.artiste').findOne({
|
||||
where: {id},
|
||||
populate: ['paroles']
|
||||
})
|
||||
return artiste
|
||||
}
|
||||
});
|
||||
return artiste;
|
||||
};
|
||||
|
||||
const validateArtiste = alias => {
|
||||
if (!alias || alias.trim().length === 0) {
|
||||
throw new ApplicationError('Champ obligatoire. Veuillez choisir un alias.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
beforeUpdate: async event => {
|
||||
let {data} = event.params
|
||||
let {data} = event.params;
|
||||
|
||||
if(!data.publishedAt) {
|
||||
validateArtiste(data.alias)
|
||||
validateArtiste(data.alias);
|
||||
|
||||
if (!data.slug || data.slug !== slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g})) {
|
||||
data.slug = slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g})
|
||||
data.slug = slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g});
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeCreate: async event => {
|
||||
let {data} = event.params
|
||||
let {data} = event.params;
|
||||
|
||||
validateArtiste(data.alias)
|
||||
validateArtiste(data.alias);
|
||||
|
||||
data.slug = slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g})
|
||||
data.slug = slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g});
|
||||
},
|
||||
afterUpdate: async event => {
|
||||
const {result} = event
|
||||
const artiste = await jwennAwtisEpiId(result.id)
|
||||
const {result} = event;
|
||||
const artiste = await jwennAwtisEpiId(result.id);
|
||||
|
||||
if (artiste.paroles && artiste.paroles.length >= 1) {
|
||||
const paroleIds = artiste.paroles.map(({id}) => id)
|
||||
const paroles = await jwennTeksEpiId(paroleIds)
|
||||
const paroleIds = artiste.paroles.map(({id}) => id);
|
||||
const paroles = await jwennTeksEpiId(paroleIds);
|
||||
await Promise.all(paroles.map(async t => {
|
||||
const {id, titre, slug, artistes} = t
|
||||
const alias = artistes.map(a => a.alias).join('-')
|
||||
const slugUpdated = slugify(`${alias}-${titre}`, {lower: true, remove: /[*#+~.()'"!:@]/g})
|
||||
const {id, titre, slug, artistes} = t;
|
||||
const alias = artistes.map(a => a.alias).join('-');
|
||||
const slugUpdated = slugify(`${alias}-${titre}`, {lower: true, remove: /[*#+~.()'"!:@]/g});
|
||||
|
||||
if (slug !== slugUpdated) {
|
||||
await strapi.db.query('api::parole.parole').update({
|
||||
@@ -62,9 +62,9 @@ module.exports = {
|
||||
data: {
|
||||
slug: slugUpdated
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}))
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {describe, it, expect, vi} from 'vitest'
|
||||
import {describe, it, expect, vi} from 'vitest';
|
||||
|
||||
const {default: createController} = await import('../artiste.js')
|
||||
const {default: createController} = await import('../artiste.js');
|
||||
|
||||
function buildStrapi({dbUser, existingArtiste = null}) {
|
||||
const dbQuery = {
|
||||
findOne: vi.fn(async () => existingArtiste)
|
||||
}
|
||||
};
|
||||
const artisteDocuments = {
|
||||
create: vi.fn(async ({data}) => ({id: 42, ...data}))
|
||||
}
|
||||
};
|
||||
const userDocuments = {
|
||||
findOne: vi.fn(async () => dbUser)
|
||||
}
|
||||
};
|
||||
|
||||
const strapi = {
|
||||
contentType: vi.fn(() => ({uid: 'api::artiste.artiste', kind: 'collectionType'})),
|
||||
@@ -19,13 +19,13 @@ function buildStrapi({dbUser, existingArtiste = null}) {
|
||||
query: vi.fn(() => dbQuery)
|
||||
},
|
||||
documents: vi.fn(uid => {
|
||||
if (uid === 'plugin::users-permissions.user') return userDocuments
|
||||
if (uid === 'api::artiste.artiste') return artisteDocuments
|
||||
throw new Error(`unexpected uid: ${uid}`)
|
||||
if (uid === 'plugin::users-permissions.user') return userDocuments;
|
||||
if (uid === 'api::artiste.artiste') return artisteDocuments;
|
||||
throw new Error(`unexpected uid: ${uid}`);
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
return {strapi, artisteDocuments}
|
||||
return {strapi, artisteDocuments};
|
||||
}
|
||||
|
||||
function buildCtx(data) {
|
||||
@@ -36,38 +36,56 @@ function buildCtx(data) {
|
||||
},
|
||||
badRequest: vi.fn(),
|
||||
notFound: vi.fn()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'}
|
||||
const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'};
|
||||
|
||||
function buildData(overrides = {}) {
|
||||
return {
|
||||
alias: 'Test Artist',
|
||||
user: {...dbUser},
|
||||
...overrides
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('artiste.create', () => {
|
||||
it('crée l\'artiste quand le user existe et que l\'alias est nouveau', async () => {
|
||||
const {strapi, artisteDocuments} = buildStrapi({dbUser})
|
||||
const controller = createController({strapi})
|
||||
const ctx = buildCtx(buildData())
|
||||
const {strapi, artisteDocuments} = buildStrapi({dbUser});
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx(buildData());
|
||||
|
||||
await controller.create(ctx)
|
||||
await controller.create(ctx);
|
||||
|
||||
expect(artisteDocuments.create).toHaveBeenCalled()
|
||||
})
|
||||
expect(artisteDocuments.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refuse sans planter quand data.user est absent', async () => {
|
||||
const {strapi} = buildStrapi({dbUser})
|
||||
const controller = createController({strapi})
|
||||
const ctx = buildCtx(buildData({user: undefined}))
|
||||
const {strapi} = buildStrapi({dbUser});
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx(buildData({user: undefined}));
|
||||
|
||||
await controller.create(ctx)
|
||||
await controller.create(ctx);
|
||||
|
||||
expect(ctx.badRequest).toHaveBeenCalled()
|
||||
expect(strapi.documents).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
expect(ctx.badRequest).toHaveBeenCalled();
|
||||
expect(strapi.documents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignore les champs non autorisés du payload (mass assignment)', async () => {
|
||||
const {strapi, artisteDocuments} = buildStrapi({dbUser});
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx(buildData({
|
||||
isExclusiveArtist: true,
|
||||
userAdmin: {id: 999}
|
||||
}));
|
||||
|
||||
await controller.create(ctx);
|
||||
|
||||
expect(artisteDocuments.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
alias: 'Test Artist',
|
||||
user: dbUser.id
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,47 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreController } = require('@strapi/strapi').factories;
|
||||
const slugify = require('slugify')
|
||||
const slugify = require('slugify');
|
||||
|
||||
const getSlug = text => {
|
||||
return slugify(text, {lower: true, remove: /[*#+~.()'"!:@]/g})
|
||||
}
|
||||
return slugify(text, {lower: true, remove: /[*#+~.()'"!:@]/g});
|
||||
};
|
||||
|
||||
module.exports = createCoreController('api::artiste.artiste', ({strapi}) => ({
|
||||
async create(ctx) {
|
||||
const {body} = ctx.request
|
||||
let {data} = body
|
||||
const {body} = ctx.request;
|
||||
let {data} = body;
|
||||
|
||||
if (!data?.user?.documentId) {
|
||||
return ctx.badRequest('Informations manquantes.')
|
||||
return ctx.badRequest('Informations manquantes.');
|
||||
}
|
||||
|
||||
const user = await strapi.documents('plugin::users-permissions.user').findOne({
|
||||
documentId: body.data.user.documentId
|
||||
})
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return ctx.notFound('Utilisateur introuvable.')
|
||||
return ctx.notFound('Utilisateur introuvable.');
|
||||
}
|
||||
|
||||
if (user.id !== data.user.id || user.username !== data.user.username || user.email !== data.user.email) {
|
||||
return ctx.badRequest('Informations non valides.')
|
||||
return ctx.badRequest('Informations non valides.');
|
||||
}
|
||||
|
||||
const artiste = await strapi.db.query('api::artiste.artiste').findOne({
|
||||
where: {slug: getSlug(data.alias)}
|
||||
})
|
||||
});
|
||||
|
||||
if (artiste) {
|
||||
return artiste
|
||||
return artiste;
|
||||
} else {
|
||||
const newArtiste = await strapi.documents('api::artiste.artiste').create({
|
||||
data: {
|
||||
...data
|
||||
alias: data.alias,
|
||||
user: user.id
|
||||
}
|
||||
})
|
||||
return newArtiste
|
||||
});
|
||||
return newArtiste;
|
||||
}
|
||||
}
|
||||
|
||||
}))
|
||||
}));
|
||||
|
||||
@@ -14,4 +14,4 @@ module.exports = createCoreRouter('api::artiste.artiste', {
|
||||
policies: [{name: 'global::is-document-owner', config: {uid: 'api::artiste.artiste'}}]
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import {describe, it, expect, vi, afterEach} from 'vitest';
|
||||
|
||||
async function loadLifecycles(strapiMock) {
|
||||
vi.resetModules();
|
||||
global.strapi = strapiMock;
|
||||
const mod = await import('../lifecycles.js');
|
||||
return mod;
|
||||
}
|
||||
|
||||
describe('commentaire afterCreate — notification email', () => {
|
||||
afterEach(() => {
|
||||
delete global.strapi;
|
||||
});
|
||||
|
||||
it('envoie le contenu en texte brut, jamais comme HTML non échappé', async () => {
|
||||
const emailSend = vi.fn();
|
||||
const strapiMock = {
|
||||
db: {
|
||||
query: vi.fn(uid => {
|
||||
if (uid === 'plugin::users-permissions.user') return {findOne: vi.fn(async () => ({id: 1, username: 'foo'}))};
|
||||
if (uid === 'api::parole.parole') return {findOne: vi.fn(async () => ({id: 7, titre: 'Mon titre'}))};
|
||||
throw new Error(`unexpected uid: ${uid}`);
|
||||
})
|
||||
},
|
||||
plugins: {email: {services: {email: {send: emailSend}}}}
|
||||
};
|
||||
|
||||
const {afterCreate} = await loadLifecycles(strapiMock);
|
||||
|
||||
await afterCreate({params: {data: {user: 1, parole: 7, contenu: '<img src=x onerror=alert(1)>'}}});
|
||||
|
||||
expect(emailSend).toHaveBeenCalledTimes(1);
|
||||
const [payload] = emailSend.mock.calls[0];
|
||||
expect(payload.text).toBe('<img src=x onerror=alert(1)>');
|
||||
expect(payload.html).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,57 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
const { ApplicationError, NotFoundError } = require("@strapi/utils").errors
|
||||
const { ApplicationError, NotFoundError } = require('@strapi/utils').errors;
|
||||
|
||||
const jwennUserEpiId = async userId => {
|
||||
if (!userId) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
||||
where: {id: userId}
|
||||
})
|
||||
});
|
||||
|
||||
return user
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
const jwennParoleEpiId = async paroleId => {
|
||||
if (!paroleId) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const parole = await strapi.db.query('api::parole.parole').findOne({
|
||||
where: {id: paroleId}
|
||||
})
|
||||
});
|
||||
|
||||
return parole
|
||||
}
|
||||
return parole;
|
||||
};
|
||||
|
||||
const validateCommentaire = data => {
|
||||
if (!data.contenu && !data.datePublication) {
|
||||
throw new ApplicationError('Mauvaise requête, contenu et datePublication sont obligatoires')
|
||||
throw new ApplicationError('Mauvaise requête, contenu et datePublication sont obligatoires');
|
||||
}
|
||||
|
||||
if (!data.contenu || data.contenu.trim().length === 0) {
|
||||
throw new ApplicationError('Champ obligatoire. Veuillez renseigner le contenu du commentaire.')
|
||||
throw new ApplicationError('Champ obligatoire. Veuillez renseigner le contenu du commentaire.');
|
||||
}
|
||||
|
||||
if (data.contenu.trim().length > 500) {
|
||||
throw new ApplicationError('Le commentaire doit contenir 500 caractères maximum.')
|
||||
throw new ApplicationError('Le commentaire doit contenir 500 caractères maximum.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
beforeCreate: async event => {
|
||||
const {data} = event.params
|
||||
validateCommentaire(data)
|
||||
const {data} = event.params;
|
||||
validateCommentaire(data);
|
||||
},
|
||||
afterCreate: async event => {
|
||||
const {data, result} = event.params
|
||||
const user = await jwennUserEpiId(data.user)
|
||||
const parole = await jwennParoleEpiId(data.parole)
|
||||
const {data} = event.params;
|
||||
const user = await jwennUserEpiId(data.user);
|
||||
const parole = await jwennParoleEpiId(data.parole);
|
||||
|
||||
if (!parole) {
|
||||
throw new NotFoundError('Texte introuvable.')
|
||||
throw new NotFoundError('Texte introuvable.');
|
||||
}
|
||||
|
||||
if (user) {
|
||||
@@ -59,8 +59,8 @@ module.exports = {
|
||||
from: process.env.SMTP_FROM,
|
||||
to: process.env.SMTP_SEND_TO,
|
||||
subject: `Commentaire de ${user.username} sur "${parole.titre}"`,
|
||||
html: data.contenu
|
||||
})
|
||||
text: data.contenu
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import {describe, it, expect, vi} from 'vitest'
|
||||
import {describe, it, expect, vi} from 'vitest';
|
||||
|
||||
const {default: createController} = await import('../commentaire.js')
|
||||
const {default: createController} = await import('../commentaire.js');
|
||||
|
||||
const dbUser = {id: 1, username: 'foo', email: 'foo@bar.com'}
|
||||
const dbParole = {id: 7, documentId: 'parole-doc-7'}
|
||||
const dbUser = {id: 1, username: 'foo', email: 'foo@bar.com'};
|
||||
const dbParole = {id: 7, documentId: 'parole-doc-7'};
|
||||
|
||||
function buildStrapi({existingParole = dbParole} = {}) {
|
||||
const commentaireDocuments = {
|
||||
create: vi.fn(async ({data}) => ({id: 99, ...data}))
|
||||
}
|
||||
};
|
||||
const paroleDocuments = {
|
||||
update: vi.fn(async () => {})
|
||||
}
|
||||
};
|
||||
const userDbQuery = {
|
||||
findOne: vi.fn(async ({where}) => (where.id === dbUser.id ? dbUser : null))
|
||||
}
|
||||
};
|
||||
const paroleDbQuery = {
|
||||
findOne: vi.fn(async ({where}) => (where.id === existingParole?.id ? existingParole : null))
|
||||
}
|
||||
};
|
||||
|
||||
const strapi = {
|
||||
contentType: vi.fn(() => ({uid: 'api::commentaire.commentaire', kind: 'collectionType'})),
|
||||
db: {
|
||||
query: vi.fn(uid => {
|
||||
if (uid === 'plugin::users-permissions.user') return userDbQuery
|
||||
if (uid === 'api::parole.parole') return paroleDbQuery
|
||||
throw new Error(`unexpected uid: ${uid}`)
|
||||
if (uid === 'plugin::users-permissions.user') return userDbQuery;
|
||||
if (uid === 'api::parole.parole') return paroleDbQuery;
|
||||
throw new Error(`unexpected uid: ${uid}`);
|
||||
})
|
||||
},
|
||||
documents: vi.fn(uid => {
|
||||
if (uid === 'api::commentaire.commentaire') return commentaireDocuments
|
||||
if (uid === 'api::parole.parole') return paroleDocuments
|
||||
throw new Error(`unexpected uid: ${uid}`)
|
||||
if (uid === 'api::commentaire.commentaire') return commentaireDocuments;
|
||||
if (uid === 'api::parole.parole') return paroleDocuments;
|
||||
throw new Error(`unexpected uid: ${uid}`);
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
return {strapi, commentaireDocuments, paroleDocuments, userDbQuery, paroleDbQuery}
|
||||
return {strapi, commentaireDocuments, paroleDocuments, userDbQuery, paroleDbQuery};
|
||||
}
|
||||
|
||||
function buildCtx(data) {
|
||||
@@ -44,7 +44,7 @@ function buildCtx(data) {
|
||||
body: {data},
|
||||
header: {authorization: 'Bearer faketoken'}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildData(overrides = {}) {
|
||||
@@ -54,49 +54,66 @@ function buildData(overrides = {}) {
|
||||
parole: dbParole.id,
|
||||
user: {...dbUser},
|
||||
...overrides
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('commentaire.create', () => {
|
||||
it('retrouve la parole par son id (pas par le documentId du user) et l\'associe correctement', async () => {
|
||||
const {strapi, commentaireDocuments, paroleDocuments, paroleDbQuery} = buildStrapi()
|
||||
const controller = createController({strapi})
|
||||
const ctx = buildCtx(buildData())
|
||||
const {strapi, commentaireDocuments, paroleDocuments, paroleDbQuery} = buildStrapi();
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx(buildData());
|
||||
|
||||
await controller.create(ctx)
|
||||
await controller.create(ctx);
|
||||
|
||||
expect(paroleDbQuery.findOne).toHaveBeenCalledWith({where: {id: dbParole.id}})
|
||||
expect(commentaireDocuments.create).toHaveBeenCalled()
|
||||
expect(paroleDbQuery.findOne).toHaveBeenCalledWith({where: {id: dbParole.id}});
|
||||
expect(commentaireDocuments.create).toHaveBeenCalled();
|
||||
expect(paroleDocuments.update).toHaveBeenCalledWith({
|
||||
documentId: dbParole.documentId,
|
||||
data: {commentaires: [99]}
|
||||
})
|
||||
})
|
||||
data: {commentaires: {connect: [99]}}
|
||||
});
|
||||
});
|
||||
|
||||
it('rejette quand la parole ciblée n\'existe pas', async () => {
|
||||
const {strapi, commentaireDocuments} = buildStrapi({existingParole: null})
|
||||
const controller = createController({strapi})
|
||||
const ctx = buildCtx(buildData())
|
||||
const {strapi, commentaireDocuments} = buildStrapi({existingParole: null});
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx(buildData());
|
||||
|
||||
await expect(controller.create(ctx)).rejects.toThrow('Texte introuvable.')
|
||||
expect(commentaireDocuments.create).not.toHaveBeenCalled()
|
||||
})
|
||||
await expect(controller.create(ctx)).rejects.toThrow('Texte introuvable.');
|
||||
expect(commentaireDocuments.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refuse sans planter quand data.user est absent', async () => {
|
||||
const {strapi, userDbQuery} = buildStrapi()
|
||||
const controller = createController({strapi})
|
||||
const ctx = buildCtx(buildData({user: undefined}))
|
||||
const {strapi, userDbQuery} = buildStrapi();
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx(buildData({user: undefined}));
|
||||
|
||||
await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.')
|
||||
expect(userDbQuery.findOne).not.toHaveBeenCalled()
|
||||
})
|
||||
await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.');
|
||||
expect(userDbQuery.findOne).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refuse sans planter quand data.parole est absent', async () => {
|
||||
const {strapi, paroleDbQuery} = buildStrapi()
|
||||
const controller = createController({strapi})
|
||||
const ctx = buildCtx(buildData({parole: undefined}))
|
||||
const {strapi, paroleDbQuery} = buildStrapi();
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx(buildData({parole: undefined}));
|
||||
|
||||
await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.')
|
||||
expect(paroleDbQuery.findOne).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.');
|
||||
expect(paroleDbQuery.findOne).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignore les champs non autorisés du payload (mass assignment)', async () => {
|
||||
const {strapi, commentaireDocuments} = buildStrapi();
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx(buildData({publishedAt: '2020-01-01'}));
|
||||
|
||||
await controller.create(ctx);
|
||||
|
||||
expect(commentaireDocuments.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
contenu: 'Un commentaire',
|
||||
datePublication: '2026-07-04',
|
||||
user: dbUser.id,
|
||||
parole: dbParole.id
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreController } = require('@strapi/strapi').factories;
|
||||
const { ApplicationError, NotFoundError } = require("@strapi/utils").errors
|
||||
const { ApplicationError, NotFoundError } = require('@strapi/utils').errors;
|
||||
|
||||
module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) => ({
|
||||
async create(ctx) {
|
||||
const {body} = ctx.request
|
||||
let {data} = body
|
||||
const {body} = ctx.request;
|
||||
let {data} = body;
|
||||
|
||||
if (!data?.user?.id || !data?.parole) {
|
||||
throw new ApplicationError('Informations manquantes.')
|
||||
throw new ApplicationError('Informations manquantes.');
|
||||
}
|
||||
|
||||
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
||||
where: {id: data.user.id}
|
||||
})
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundError('Utilisateur introuvable.')
|
||||
throw new NotFoundError('Utilisateur introuvable.');
|
||||
}
|
||||
|
||||
if (user.id !== data.user.id || user.username !== data.user.username || user.email !== data.user.email) {
|
||||
throw new ApplicationError('Informations non valides.')
|
||||
throw new ApplicationError('Informations non valides.');
|
||||
}
|
||||
|
||||
data.user = user.id
|
||||
|
||||
const parole = await strapi.db.query('api::parole.parole').findOne({
|
||||
where: {id: data.parole}
|
||||
})
|
||||
});
|
||||
|
||||
if (!parole) {
|
||||
throw new NotFoundError('Texte introuvable.')
|
||||
throw new NotFoundError('Texte introuvable.');
|
||||
}
|
||||
|
||||
const newCommentaire = await strapi.documents('api::commentaire.commentaire').create({
|
||||
data: {
|
||||
...data
|
||||
contenu: data.contenu,
|
||||
datePublication: data.datePublication,
|
||||
user: user.id,
|
||||
parole: parole.id
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
await strapi.documents('api::parole.parole').update({
|
||||
documentId: parole.documentId,
|
||||
|
||||
data: {
|
||||
commentaires: [newCommentaire.id]
|
||||
commentaires: {connect: [newCommentaire.id]}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return newCommentaire;
|
||||
}
|
||||
}))
|
||||
}));
|
||||
|
||||
@@ -14,4 +14,4 @@ module.exports = createCoreRouter('api::commentaire.commentaire', {
|
||||
policies: [{name: 'global::is-document-owner', config: {uid: 'api::commentaire.commentaire'}}]
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
import {describe, it, expect, vi, afterEach} from 'vitest'
|
||||
import {describe, it, expect, vi, afterEach} from 'vitest';
|
||||
|
||||
async function loadLifecycles(strapiMock) {
|
||||
vi.resetModules()
|
||||
global.strapi = strapiMock
|
||||
const mod = await import('../lifecycles.js')
|
||||
return mod
|
||||
vi.resetModules();
|
||||
global.strapi = strapiMock;
|
||||
const mod = await import('../lifecycles.js');
|
||||
return mod;
|
||||
}
|
||||
|
||||
describe('afterCreate — notification au soumetteur', () => {
|
||||
afterEach(() => {
|
||||
delete global.strapi
|
||||
})
|
||||
delete global.strapi;
|
||||
});
|
||||
|
||||
it('recherche le user par id (pas par un champ "user" inexistant) et envoie le mail', async () => {
|
||||
const userFindOne = vi.fn(async ({where}) => {
|
||||
if (where.id === 5) return {id: 5, username: 'foo', email: 'foo@bar.com'}
|
||||
return null
|
||||
})
|
||||
const emailSend = vi.fn()
|
||||
if (where.id === 5) return {id: 5, username: 'foo', email: 'foo@bar.com'};
|
||||
return null;
|
||||
});
|
||||
const emailSend = vi.fn();
|
||||
|
||||
const strapiMock = {
|
||||
db: {
|
||||
query: vi.fn(uid => {
|
||||
if (uid === 'plugin::users-permissions.user') return {findOne: userFindOne}
|
||||
return {findOne: vi.fn(async () => null)}
|
||||
if (uid === 'plugin::users-permissions.user') return {findOne: userFindOne};
|
||||
return {findOne: vi.fn(async () => null)};
|
||||
})
|
||||
},
|
||||
plugins: {
|
||||
email: {services: {email: {send: emailSend}}}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const {afterCreate} = await loadLifecycles(strapiMock)
|
||||
const {afterCreate} = await loadLifecycles(strapiMock);
|
||||
|
||||
await afterCreate({params: {data: {titre: 'Titre', user: {id: 5}}}})
|
||||
await afterCreate({params: {data: {titre: 'Titre', user: {id: 5}}}});
|
||||
|
||||
expect(userFindOne).toHaveBeenCalledWith({where: {id: 5}})
|
||||
expect(emailSend).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
expect(userFindOne).toHaveBeenCalledWith({where: {id: 5}});
|
||||
expect(emailSend).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforeUpdate — notifications Telegram/Revolt', () => {
|
||||
const originalEnv = {...process.env}
|
||||
const axios = require('axios')
|
||||
const originalPost = axios.post
|
||||
const originalEnv = {...process.env};
|
||||
const axios = require('axios');
|
||||
const originalPost = axios.post;
|
||||
|
||||
function buildStrapi(previous) {
|
||||
const dbQuery = {
|
||||
findOne: vi.fn(async () => previous),
|
||||
updateMany: vi.fn()
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
db: {query: vi.fn(() => dbQuery)},
|
||||
plugins: {email: {services: {email: {send: vi.fn()}}}},
|
||||
log: {error: vi.fn()}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildEvent(overrides = {}) {
|
||||
@@ -64,79 +64,104 @@ describe('beforeUpdate — notifications Telegram/Revolt', () => {
|
||||
params: {
|
||||
data: {documentId: 'doc-1', publishedAt: '2026-07-04T00:00:00.000Z', ...overrides}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
process.env = {...originalEnv}
|
||||
axios.post = originalPost
|
||||
delete global.strapi
|
||||
})
|
||||
process.env = {...originalEnv};
|
||||
axios.post = originalPost;
|
||||
delete global.strapi;
|
||||
});
|
||||
|
||||
it("n'interrompt pas la publication quand Telegram échoue, et encode le message", async () => {
|
||||
process.env.TELEGRAM_API_TOKEN = 'fake-token'
|
||||
process.env.TELEGRAM_CHAN_ID = 'fake-chan'
|
||||
delete process.env.REVOLT_TOKEN
|
||||
it('n\'interrompt pas la publication quand Telegram échoue, et encode le message', async () => {
|
||||
process.env.TELEGRAM_API_TOKEN = 'fake-token';
|
||||
process.env.TELEGRAM_CHAN_ID = 'fake-chan';
|
||||
delete process.env.REVOLT_TOKEN;
|
||||
|
||||
axios.post = vi.fn(async () => {
|
||||
throw new Error('boom')
|
||||
})
|
||||
throw new Error('boom');
|
||||
});
|
||||
|
||||
const previous = {publishedAt: null, slug: 'foo&bar', titre: 'Mon titre', user: null, userAdmin: null, artistes: []}
|
||||
const strapiMock = buildStrapi(previous)
|
||||
const previous = {publishedAt: null, slug: 'foo&bar', titre: 'Mon titre', user: null, userAdmin: null, artistes: []};
|
||||
const strapiMock = buildStrapi(previous);
|
||||
|
||||
const {beforeUpdate} = await loadLifecycles(strapiMock)
|
||||
const {beforeUpdate} = await loadLifecycles(strapiMock);
|
||||
|
||||
await beforeUpdate(buildEvent())
|
||||
await beforeUpdate(buildEvent());
|
||||
|
||||
expect(axios.post).toHaveBeenCalledTimes(1)
|
||||
const [calledUrl] = axios.post.mock.calls[0]
|
||||
expect(calledUrl).toContain('text=')
|
||||
expect(calledUrl.split('text=')[1]).not.toContain('&bar')
|
||||
expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Telegram'))
|
||||
})
|
||||
expect(axios.post).toHaveBeenCalledTimes(1);
|
||||
const [calledUrl] = axios.post.mock.calls[0];
|
||||
expect(calledUrl).toContain('text=');
|
||||
expect(calledUrl.split('text=')[1]).not.toContain('&bar');
|
||||
expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Telegram'));
|
||||
});
|
||||
|
||||
it("n'interrompt pas la publication quand Revolt échoue", async () => {
|
||||
delete process.env.TELEGRAM_API_TOKEN
|
||||
process.env.REVOLT_TOKEN = 'fake-token'
|
||||
process.env.REVOLT_TARGET = 'fake-target'
|
||||
process.env.REVOLT_BOT_ID = 'fake-bot'
|
||||
it('n\'interrompt pas la publication quand Revolt échoue', async () => {
|
||||
delete process.env.TELEGRAM_API_TOKEN;
|
||||
process.env.REVOLT_TOKEN = 'fake-token';
|
||||
process.env.REVOLT_TARGET = 'fake-target';
|
||||
process.env.REVOLT_BOT_ID = 'fake-bot';
|
||||
|
||||
axios.post = vi.fn(async () => {
|
||||
throw new Error('boom')
|
||||
})
|
||||
throw new Error('boom');
|
||||
});
|
||||
|
||||
const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: []}
|
||||
const strapiMock = buildStrapi(previous)
|
||||
const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: []};
|
||||
const strapiMock = buildStrapi(previous);
|
||||
|
||||
const {beforeUpdate} = await loadLifecycles(strapiMock)
|
||||
const {beforeUpdate} = await loadLifecycles(strapiMock);
|
||||
|
||||
await beforeUpdate(buildEvent())
|
||||
await beforeUpdate(buildEvent());
|
||||
|
||||
expect(axios.post).toHaveBeenCalledTimes(1)
|
||||
expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Revolt'))
|
||||
})
|
||||
})
|
||||
expect(axios.post).toHaveBeenCalledTimes(1);
|
||||
expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Revolt'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforeUpdate — createdBy/updatedBy', () => {
|
||||
afterEach(() => {
|
||||
delete global.strapi;
|
||||
});
|
||||
|
||||
it('retire createdBy/updatedBy du payload avant la mise à jour', async () => {
|
||||
const dbQuery = {findOne: vi.fn(async () => ({publishedAt: null, artistes: []}))};
|
||||
const strapiMock = {db: {query: vi.fn(() => dbQuery)}};
|
||||
|
||||
const {beforeUpdate} = await loadLifecycles(strapiMock);
|
||||
|
||||
const event = {
|
||||
state: {},
|
||||
params: {
|
||||
data: {documentId: 'doc-1', createdBy: 999, updatedBy: 999}
|
||||
}
|
||||
};
|
||||
|
||||
await beforeUpdate(event);
|
||||
|
||||
expect(event.params.data.createdBy).toBeUndefined();
|
||||
expect(event.params.data.updatedBy).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterUpdate — historique de différence', () => {
|
||||
afterEach(() => {
|
||||
delete global.strapi
|
||||
})
|
||||
delete global.strapi;
|
||||
});
|
||||
|
||||
it("n'échoue pas quand updatedBy n'est pas peuplé", async () => {
|
||||
const entityServiceUpdate = vi.fn(async () => {})
|
||||
it('n\'échoue pas quand updatedBy n\'est pas peuplé', async () => {
|
||||
const entityServiceUpdate = vi.fn(async () => {});
|
||||
const strapiMock = {
|
||||
entityService: {update: entityServiceUpdate}
|
||||
}
|
||||
};
|
||||
|
||||
const {afterUpdate} = await loadLifecycles(strapiMock)
|
||||
const {afterUpdate} = await loadLifecycles(strapiMock);
|
||||
|
||||
const event = {
|
||||
result: {id: 1, difference: [], updatedBy: undefined},
|
||||
state: {diff: {path: 'transcription', jsonDiff: []}}
|
||||
}
|
||||
};
|
||||
|
||||
await afterUpdate(event)
|
||||
await afterUpdate(event);
|
||||
|
||||
expect(entityServiceUpdate).toHaveBeenCalledWith('api::parole.parole', 1, {
|
||||
data: {
|
||||
@@ -148,6 +173,6 @@ describe('afterUpdate — historique de différence', () => {
|
||||
sources: 'transcription'
|
||||
}]
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
const slugify = require('slugify')
|
||||
const axios = require('axios')
|
||||
const slugify = require('slugify');
|
||||
const axios = require('axios');
|
||||
|
||||
const utils = require('@strapi/utils')
|
||||
const { ApplicationError } = utils.errors
|
||||
const utils = require('@strapi/utils');
|
||||
const { ApplicationError } = utils.errors;
|
||||
|
||||
const TELEGRAM_API_URL = 'https://api.telegram.org'
|
||||
const TELEGRAM_CHAN_ID = process.env.TELEGRAM_CHAN_ID || null
|
||||
const TELEGRAM_API_TOKEN = process.env.TELEGRAM_API_TOKEN || null
|
||||
const MESSAGE_URL = `${TELEGRAM_API_URL}/bot${TELEGRAM_API_TOKEN}/sendMessage?chat_id=${TELEGRAM_CHAN_ID}&parse_mode=html`
|
||||
const REVOLT_BOT_ID = process.env.REVOLT_BOT_ID || null
|
||||
const REVOLT_TARGET = process.env.REVOLT_TARGET || null
|
||||
const REVOLT_TOKEN = process.env.REVOLT_TOKEN || null
|
||||
const TELEGRAM_API_URL = 'https://api.telegram.org';
|
||||
const TELEGRAM_CHAN_ID = process.env.TELEGRAM_CHAN_ID || null;
|
||||
const TELEGRAM_API_TOKEN = process.env.TELEGRAM_API_TOKEN || null;
|
||||
const MESSAGE_URL = `${TELEGRAM_API_URL}/bot${TELEGRAM_API_TOKEN}/sendMessage?chat_id=${TELEGRAM_CHAN_ID}&parse_mode=html`;
|
||||
const REVOLT_BOT_ID = process.env.REVOLT_BOT_ID || null;
|
||||
const REVOLT_TARGET = process.env.REVOLT_TARGET || null;
|
||||
const REVOLT_TOKEN = process.env.REVOLT_TOKEN || null;
|
||||
|
||||
const getSlug = (artiste, parole) => {
|
||||
return slugify(`${artiste}-${parole}`, {lower: true, remove: /[*#+~.()'"!:@]/g})
|
||||
}
|
||||
return slugify(`${artiste}-${parole}`, {lower: true, remove: /[*#+~.()'"!:@]/g});
|
||||
};
|
||||
|
||||
const isSlugExists = async existingSlug => {
|
||||
const slugs = await strapi.db.query('api::parole.parole').count({
|
||||
@@ -25,10 +25,10 @@ const isSlugExists = async existingSlug => {
|
||||
$eq: existingSlug
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return Boolean(slugs)
|
||||
}
|
||||
return Boolean(slugs);
|
||||
};
|
||||
|
||||
const jwennAwtisEpiId = async artistesIds => {
|
||||
if (!artistesIds || artistesIds.length === 0) {
|
||||
@@ -42,30 +42,30 @@ const jwennAwtisEpiId = async artistesIds => {
|
||||
$in: artistesIds.map(id => id)
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return artistes.map(a => a.alias).join('-')
|
||||
}
|
||||
return artistes.map(a => a.alias).join('-');
|
||||
};
|
||||
|
||||
const jwennUserEpiId = async userId => {
|
||||
if (!userId) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
||||
where: {id: userId}
|
||||
})
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new ApplicationError('Utilisateur introuvable.')
|
||||
throw new ApplicationError('Utilisateur introuvable.');
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
const jwennUserAdminEpiId = async userAdminId => {
|
||||
if (!userAdminId) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const userAdmin = await strapi.db.query('admin::user').findOne({
|
||||
@@ -83,14 +83,14 @@ const jwennUserAdminEpiId = async userAdminId => {
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return userAdmin
|
||||
}
|
||||
return userAdmin;
|
||||
};
|
||||
|
||||
const jwennSuperAdminEpiId = async userAdminId => {
|
||||
if (!userAdminId) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const userAdmin = await strapi.db.query('admin::user').findOne({
|
||||
@@ -108,87 +108,91 @@ const jwennSuperAdminEpiId = async userAdminId => {
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return userAdmin
|
||||
}
|
||||
return userAdmin;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
beforeCreate: async event => {
|
||||
let {data} = event.params
|
||||
let {data} = event.params;
|
||||
|
||||
delete data.createdBy
|
||||
delete data.updatedBy
|
||||
delete data.createdBy;
|
||||
delete data.updatedBy;
|
||||
|
||||
strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription)
|
||||
strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription);
|
||||
|
||||
let artistesIds = []
|
||||
let artistesIds = [];
|
||||
|
||||
if (data?.artistes?.connect?.length) {
|
||||
artistesIds = data.artistes.connect.map(a => a.id)
|
||||
artistesIds = data.artistes.connect.map(a => a.id);
|
||||
|
||||
if (data.titre && !data.forceSlug) {
|
||||
const artiste = await jwennAwtisEpiId(artistesIds)
|
||||
data.slug = getSlug(artiste, data.titre)
|
||||
const artiste = await jwennAwtisEpiId(artistesIds);
|
||||
data.slug = getSlug(artiste, data.titre);
|
||||
}
|
||||
|
||||
const getSlugExistance = await isSlugExists(data.slug)
|
||||
const getSlugExistance = await isSlugExists(data.slug);
|
||||
|
||||
if (getSlugExistance) {
|
||||
throw new ApplicationError('Un morceau du même artiste existe déjà.')
|
||||
throw new ApplicationError('Un morceau du même artiste existe déjà.');
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUpdate: async event => {
|
||||
const {state} = event
|
||||
let {data} = event.params
|
||||
const {documentId} = data
|
||||
const {state} = event;
|
||||
let {data} = event.params;
|
||||
|
||||
delete data.createdBy;
|
||||
delete data.updatedBy;
|
||||
|
||||
const {documentId} = data;
|
||||
|
||||
if (data.isNewRelease === true) {
|
||||
await strapi.db.query('api::parole.parole').updateMany({
|
||||
where: { isNewRelease: true },
|
||||
data: { isNewRelease: false },
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const previousParoles = await strapi.db.query('api::parole.parole').findOne({
|
||||
where: {documentId},
|
||||
populate: {difference: true, artistes: true}
|
||||
})
|
||||
});
|
||||
|
||||
if (data.transcription && previousParoles.publishedAt) {
|
||||
const difference = strapi.service('api::parole.parole').parolesDiff(data.titre, previousParoles.transcription, data.transcription)
|
||||
if (data.transcription && previousParoles.publishedAt) {
|
||||
const difference = strapi.service('api::parole.parole').parolesDiff(data.titre, previousParoles.transcription, data.transcription);
|
||||
|
||||
state.diff = difference
|
||||
}
|
||||
state.diff = difference;
|
||||
}
|
||||
|
||||
if(!data.publishedAt && data.titre && data.transcription) {
|
||||
strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription)
|
||||
strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription);
|
||||
if (data.titre && !data.forceSlug) {
|
||||
let artistes
|
||||
let artistes;
|
||||
if (data.artistes.connect.length === 0) {
|
||||
artistes = previousParoles.artistes.map(a => a.alias).join('-')
|
||||
artistes = previousParoles.artistes.map(a => a.alias).join('-');
|
||||
} else {
|
||||
let artistesIds = []
|
||||
let artistesIds = [];
|
||||
|
||||
artistesIds = data.artistes.connect.map(a => a.id)
|
||||
artistes = await jwennAwtisEpiId(artistesIds)
|
||||
artistesIds = data.artistes.connect.map(a => a.id);
|
||||
artistes = await jwennAwtisEpiId(artistesIds);
|
||||
}
|
||||
|
||||
data.slug = getSlug(artistes, data.titre)
|
||||
data.slug = getSlug(artistes, data.titre);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.publishedAt != null) {
|
||||
const previousData = await strapi.db.query('api::parole.parole').findOne({
|
||||
where: {documentId}
|
||||
})
|
||||
});
|
||||
|
||||
const previousPublishedAt = previousData.publishedAt
|
||||
const currentPublished_at = data.publishedAt
|
||||
const previousPublishedAt = previousData.publishedAt;
|
||||
const currentPublished_at = data.publishedAt;
|
||||
if (currentPublished_at != previousPublishedAt) {
|
||||
const message = `<b>Nouvelle publication</b> ❤️
|
||||
\n${process.env.WEBSITE_URL}/paroles/${previousData.slug}`
|
||||
\n${process.env.WEBSITE_URL}/paroles/${previousData.slug}`;
|
||||
if (previousData.user) {
|
||||
strapi.plugins['email'].services.email.send({
|
||||
from: process.env.SMTP_FROM,
|
||||
@@ -199,7 +203,7 @@ module.exports = {
|
||||
Merci pour votre contribution ❤️`,
|
||||
html: `<p>Le titre que vous avez soumis, <strong>"${previousData.titre}"</strong> a été publié sur le site.</p>
|
||||
<p>Vous pouvez le trouver à l'adresse <a href="${process.env.WEBSITE_URL}/paroles/${previousData.slug}">${process.env.WEBSITE_URL}/paroles/${previousData.slug}</a>.</p><p>Merci pour votre contribution ❤️</p>`
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (previousData.userAdmin) {
|
||||
@@ -212,66 +216,66 @@ module.exports = {
|
||||
Merci pour votre contribution ❤️`,
|
||||
html: `<p>Le titre que vous avez soumis, <strong>"${previousData.titre}"</strong> a été publié sur le site.</p>
|
||||
<p>Vous pouvez le trouver à l'adresse <a href="${process.env.WEBSITE_URL}/paroles/${previousData.slug}">${process.env.WEBSITE_URL}/paroles/${previousData.slug}</a>.</p><p>Merci pour votre contribution ❤️</p>`
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (TELEGRAM_API_TOKEN) {
|
||||
try {
|
||||
await axios.post(`${MESSAGE_URL}&text=${encodeURIComponent(message)}`)
|
||||
await axios.post(`${MESSAGE_URL}&text=${encodeURIComponent(message)}`);
|
||||
} catch (err) {
|
||||
strapi.log.error(`Notification Telegram : ${err.message}`)
|
||||
strapi.log.error(`Notification Telegram : ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (REVOLT_TOKEN && REVOLT_TARGET && REVOLT_BOT_ID) {
|
||||
const revoltMessage = `Nouvelle publication
|
||||
\n${process.env.WEBSITE_URL}/paroles/${previousData.slug}`
|
||||
\n${process.env.WEBSITE_URL}/paroles/${previousData.slug}`;
|
||||
|
||||
const targetChannel = REVOLT_TARGET
|
||||
const botToken = REVOLT_TOKEN
|
||||
const url = `https://api.revolt.chat/channels/${targetChannel}/messages`
|
||||
const targetChannel = REVOLT_TARGET;
|
||||
const botToken = REVOLT_TOKEN;
|
||||
const url = `https://api.revolt.chat/channels/${targetChannel}/messages`;
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
'X-Bot-Token': botToken,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await axios.post(url, {content: revoltMessage}, config)
|
||||
await axios.post(url, {content: revoltMessage}, config);
|
||||
} catch (err) {
|
||||
strapi.log.error(`Notification Revolt : ${err.message}`)
|
||||
strapi.log.error(`Notification Revolt : ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
afterUpdate: async event => {
|
||||
const {result, state} = event
|
||||
const {result, state} = event;
|
||||
|
||||
if (state.diff) {
|
||||
await strapi.entityService.update('api::parole.parole', result.id, {
|
||||
data: {
|
||||
difference: [
|
||||
...result.difference,
|
||||
{
|
||||
admin_user: result.updatedBy?.id ?? null,
|
||||
paroles: state.diff.path,
|
||||
jsonDiff: state.diff.jsonDiff,
|
||||
date: new Date(),
|
||||
sources: 'transcription'
|
||||
}]
|
||||
...result.difference,
|
||||
{
|
||||
admin_user: result.updatedBy?.id ?? null,
|
||||
paroles: state.diff.path,
|
||||
jsonDiff: state.diff.jsonDiff,
|
||||
date: new Date(),
|
||||
sources: 'transcription'
|
||||
}]
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
afterCreate: async event => {
|
||||
const {data} = event.params
|
||||
const user = await jwennUserEpiId(data?.user?.id)
|
||||
const userAdmin = await jwennUserAdminEpiId(data?.createdBy)
|
||||
const superAdmin = await jwennSuperAdminEpiId(data?.createdBy)
|
||||
const traductionsId = data?.traductions?.id
|
||||
const {data} = event.params;
|
||||
const user = await jwennUserEpiId(data?.user?.id);
|
||||
const userAdmin = await jwennUserAdminEpiId(data?.createdBy);
|
||||
const superAdmin = await jwennSuperAdminEpiId(data?.createdBy);
|
||||
const traductionsId = data?.traductions?.id;
|
||||
|
||||
if (traductionsId) {
|
||||
const result = await strapi.db.query('api::parole.parole').findOne({
|
||||
@@ -283,15 +287,15 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
populate: {traductions: true, artistes: true}
|
||||
})
|
||||
});
|
||||
|
||||
if (superAdmin && data.traductionAuto && result.traductions.francais && (!result.traductions.anglais || !result.traductions.espagnol || !result.traductions.allemand || !result.traductions.italien)) {
|
||||
const traductions = await strapi.service('api::parole.parole').translateLyrics(result.traductions.francais)
|
||||
const traductions = await strapi.service('api::parole.parole').translateLyrics(result.traductions.francais);
|
||||
await strapi.entityService.update('api::parole.parole', result.id, {
|
||||
data: {
|
||||
traductions
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +306,7 @@ module.exports = {
|
||||
subject: `Nouveau texte de ${user.username} : "${data.titre}" (site)`,
|
||||
text: `Le titre "${data.titre}" a été soumis depuis le site.`,
|
||||
html: `Le titre <strong>"${data.titre}"</strong> a été soumis depuis le site.`
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (userAdmin) {
|
||||
@@ -312,7 +316,7 @@ module.exports = {
|
||||
subject: `Nouveau texte de ${userAdmin.firstname} : "${data.titre}" (site)`,
|
||||
text: `Le titre "${data.titre}" a été soumis depuis le site.`,
|
||||
html: `Le titre <strong>"${data.titre}"</strong> a été soumis depuis le site.`
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import {describe, it, expect, vi} from 'vitest'
|
||||
import {describe, it, expect, vi} from 'vitest';
|
||||
|
||||
const {default: createController} = await import('../parole.js')
|
||||
const {default: createController} = await import('../parole.js');
|
||||
|
||||
function buildStrapi({dbUser, artiste}) {
|
||||
const paroleDocuments = {
|
||||
findMany: vi.fn(async () => []),
|
||||
create: vi.fn(async ({data}) => ({id: 42, ...data}))
|
||||
}
|
||||
create: vi.fn(async ({data}) => ({id: 42, ...data})),
|
||||
update: vi.fn(async ({data}) => ({id: 42, ...data}))
|
||||
};
|
||||
const userDocuments = {
|
||||
findOne: vi.fn(async () => dbUser),
|
||||
update: vi.fn(async () => {})
|
||||
}
|
||||
};
|
||||
const artisteDocuments = {
|
||||
findOne: vi.fn(async () => artiste)
|
||||
}
|
||||
};
|
||||
|
||||
const strapi = {
|
||||
contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'})),
|
||||
@@ -22,14 +23,14 @@ function buildStrapi({dbUser, artiste}) {
|
||||
translateLyrics: vi.fn()
|
||||
})),
|
||||
documents: vi.fn(uid => {
|
||||
if (uid === 'plugin::users-permissions.user') return userDocuments
|
||||
if (uid === 'api::artiste.artiste') return artisteDocuments
|
||||
if (uid === 'api::parole.parole') return paroleDocuments
|
||||
throw new Error(`unexpected uid: ${uid}`)
|
||||
if (uid === 'plugin::users-permissions.user') return userDocuments;
|
||||
if (uid === 'api::artiste.artiste') return artisteDocuments;
|
||||
if (uid === 'api::parole.parole') return paroleDocuments;
|
||||
throw new Error(`unexpected uid: ${uid}`);
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
return {strapi, paroleDocuments, userDocuments, artisteDocuments}
|
||||
return {strapi, paroleDocuments, userDocuments, artisteDocuments};
|
||||
}
|
||||
|
||||
function buildCtx(data) {
|
||||
@@ -40,11 +41,11 @@ function buildCtx(data) {
|
||||
},
|
||||
badRequest: vi.fn(),
|
||||
notFound: vi.fn()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'}
|
||||
const artiste = {documentId: 'artiste-doc-1'}
|
||||
const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'};
|
||||
const artiste = {id: 9, documentId: 'artiste-doc-1'};
|
||||
|
||||
function buildData(overrides = {}) {
|
||||
return {
|
||||
@@ -53,39 +54,117 @@ function buildData(overrides = {}) {
|
||||
user: {...dbUser},
|
||||
artistes: [{documentId: 'artiste-doc-1'}],
|
||||
...overrides
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('parole.findOne', () => {
|
||||
it('interroge avec le documentId venant de ctx.params.id, pas avec ctx lui-même', async () => {
|
||||
const paroleDocuments = {
|
||||
findOne: vi.fn(async ({documentId}) => ({id: 1, documentId, titre: 'Test'}))
|
||||
};
|
||||
const strapi = {
|
||||
contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'})),
|
||||
documents: vi.fn(uid => {
|
||||
if (uid === 'api::parole.parole') return paroleDocuments;
|
||||
throw new Error(`unexpected uid: ${uid}`);
|
||||
})
|
||||
};
|
||||
const controller = createController({strapi});
|
||||
const ctx = {params: {id: 'doc-123'}};
|
||||
|
||||
const result = await controller.findOne(ctx);
|
||||
|
||||
expect(paroleDocuments.findOne).toHaveBeenCalledWith({
|
||||
documentId: 'doc-123',
|
||||
populate: ['artistes']
|
||||
});
|
||||
expect(result).toEqual({id: 1, documentId: 'doc-123', titre: 'Test'});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parole.create', () => {
|
||||
it('crée la parole quand le user et l\'artiste existent', async () => {
|
||||
const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste})
|
||||
const controller = createController({strapi})
|
||||
const ctx = buildCtx(buildData())
|
||||
const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste});
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx(buildData());
|
||||
|
||||
await controller.create(ctx)
|
||||
await controller.create(ctx);
|
||||
|
||||
expect(paroleDocuments.create).toHaveBeenCalled()
|
||||
})
|
||||
expect(paroleDocuments.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refuse sans planter quand data.user est absent', async () => {
|
||||
const {strapi, userDocuments} = buildStrapi({dbUser, artiste})
|
||||
const controller = createController({strapi})
|
||||
const ctx = buildCtx(buildData({user: undefined}))
|
||||
const {strapi, userDocuments} = buildStrapi({dbUser, artiste});
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx(buildData({user: undefined}));
|
||||
|
||||
await controller.create(ctx)
|
||||
await controller.create(ctx);
|
||||
|
||||
expect(ctx.badRequest).toHaveBeenCalled()
|
||||
expect(userDocuments.findOne).not.toHaveBeenCalled()
|
||||
})
|
||||
expect(ctx.badRequest).toHaveBeenCalled();
|
||||
expect(userDocuments.findOne).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refuse sans planter quand data.artistes est vide', async () => {
|
||||
const {strapi, artisteDocuments} = buildStrapi({dbUser, artiste})
|
||||
const controller = createController({strapi})
|
||||
const ctx = buildCtx(buildData({artistes: []}))
|
||||
const {strapi, artisteDocuments} = buildStrapi({dbUser, artiste});
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx(buildData({artistes: []}));
|
||||
|
||||
await controller.create(ctx)
|
||||
await controller.create(ctx);
|
||||
|
||||
expect(ctx.badRequest).toHaveBeenCalled()
|
||||
expect(artisteDocuments.findOne).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
expect(ctx.badRequest).toHaveBeenCalled();
|
||||
expect(artisteDocuments.findOne).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignore les champs non autorisés du payload (mass assignment)', async () => {
|
||||
const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste});
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx(buildData({
|
||||
userAdmin: {id: 999},
|
||||
isNewRelease: true,
|
||||
difference: [{fake: true}]
|
||||
}));
|
||||
|
||||
await controller.create(ctx);
|
||||
|
||||
expect(paroleDocuments.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
titre: 'Test',
|
||||
transcription: 'Paroles...',
|
||||
traductions: undefined,
|
||||
traductionAuto: undefined,
|
||||
artistes: [artiste.id],
|
||||
user: dbUser.id
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parole.update', () => {
|
||||
it('ignore les champs non autorisés du payload (mass assignment)', async () => {
|
||||
const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste});
|
||||
const controller = createController({strapi});
|
||||
const ctx = buildCtx({
|
||||
documentId: 'doc-1',
|
||||
titre: 'Nouveau titre',
|
||||
transcription: 'Nouveau texte',
|
||||
traductions: {francais: 'salut'},
|
||||
traductionAuto: true,
|
||||
artistes: [9],
|
||||
userAdmin: {id: 999},
|
||||
user: {id: 999}
|
||||
});
|
||||
|
||||
await controller.update(ctx);
|
||||
|
||||
expect(paroleDocuments.update).toHaveBeenCalledWith({
|
||||
documentId: 'doc-1',
|
||||
data: {
|
||||
titre: 'Nouveau titre',
|
||||
transcription: 'Nouveau texte',
|
||||
traductions: {francais: 'salut'},
|
||||
traductionAuto: true,
|
||||
artistes: [9]
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
|
||||
const { createCoreController } = require('@strapi/strapi').factories;
|
||||
|
||||
const VALID_LANGS = new Set(['fr', 'en', 'es', 'de', 'it'])
|
||||
const VALID_LANGS = new Set(['fr', 'en', 'es', 'de', 'it']);
|
||||
|
||||
module.exports = createCoreController('api::parole.parole', ({strapi}) => ({
|
||||
async export(ctx) {
|
||||
const { type = 'pairs', lang, format = 'jsonl' } = ctx.query
|
||||
const { type = 'pairs', lang, format = 'jsonl' } = ctx.query;
|
||||
|
||||
const langs = lang
|
||||
? lang.split(',').map(l => l.trim()).filter(l => VALID_LANGS.has(l))
|
||||
: null
|
||||
: null;
|
||||
|
||||
if (lang && (!langs || langs.length === 0)) {
|
||||
return ctx.badRequest('Langue(s) invalide(s). Valeurs acceptées : fr, en, es, de, it.')
|
||||
return ctx.badRequest('Langue(s) invalide(s). Valeurs acceptées : fr, en, es, de, it.');
|
||||
}
|
||||
|
||||
if (!['pairs', 'instruct'].includes(type)) {
|
||||
return ctx.badRequest('type invalide. Valeurs acceptées : pairs, instruct.')
|
||||
return ctx.badRequest('type invalide. Valeurs acceptées : pairs, instruct.');
|
||||
}
|
||||
|
||||
const paroles = await strapi.service('api::parole.parole').fetchAllParoles()
|
||||
const { metadata, pairs } = strapi.service('api::parole.parole').buildExport(paroles, type, langs)
|
||||
const paroles = await strapi.service('api::parole.parole').fetchAllParoles();
|
||||
const { metadata, pairs } = strapi.service('api::parole.parole').buildExport(paroles, type, langs);
|
||||
|
||||
if (format === 'json') {
|
||||
return ctx.send({ metadata, data: pairs })
|
||||
return ctx.send({ metadata, data: pairs });
|
||||
}
|
||||
|
||||
// JSONL : première ligne = métadonnées, suivies des exemples d'entraînement.
|
||||
@@ -32,68 +32,73 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({
|
||||
const lines = [
|
||||
JSON.stringify({ _metadata: true, ...metadata }),
|
||||
...pairs.map(p => JSON.stringify(p)),
|
||||
]
|
||||
];
|
||||
|
||||
ctx.set('Content-Type', 'application/x-ndjson')
|
||||
ctx.set('Content-Disposition', `attachment; filename="pawol-nu-export-${Date.now()}.jsonl"`)
|
||||
ctx.body = lines.join('\n')
|
||||
ctx.set('Content-Type', 'application/x-ndjson');
|
||||
ctx.set('Content-Disposition', `attachment; filename="pawol-nu-export-${Date.now()}.jsonl"`);
|
||||
ctx.body = lines.join('\n');
|
||||
},
|
||||
|
||||
async bulkTranslate(ctx) {
|
||||
const result = await strapi.service('api::parole.parole').bulkTranslateMissing()
|
||||
return ctx.send(result)
|
||||
const result = await strapi.service('api::parole.parole').bulkTranslateMissing();
|
||||
return ctx.send(result);
|
||||
},
|
||||
|
||||
async findOne(documentId) {
|
||||
async findOne(ctx) {
|
||||
const {id: documentId} = ctx.params;
|
||||
const parole = await strapi.documents('api::parole.parole').findOne({
|
||||
documentId,
|
||||
populate: ['artistes']
|
||||
})
|
||||
});
|
||||
|
||||
return parole
|
||||
return parole;
|
||||
},
|
||||
async update(ctx) {
|
||||
const {body} = ctx.request
|
||||
const {data} = body
|
||||
const {body} = ctx.request;
|
||||
const {data} = body;
|
||||
|
||||
const updatedParole = await strapi.documents('api::parole.parole').update({
|
||||
documentId: data.documentId,
|
||||
|
||||
data: {
|
||||
...data
|
||||
titre: data.titre,
|
||||
transcription: data.transcription,
|
||||
traductions: data.traductions,
|
||||
traductionAuto: data.traductionAuto,
|
||||
artistes: data.artistes
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return updatedParole
|
||||
return updatedParole;
|
||||
},
|
||||
async create(ctx) {
|
||||
const {body} = ctx.request
|
||||
const {data} = body
|
||||
const {body} = ctx.request;
|
||||
const {data} = body;
|
||||
|
||||
if (!data?.user?.documentId || !data?.artistes?.[0]?.documentId) {
|
||||
return ctx.badRequest('Informations manquantes.')
|
||||
return ctx.badRequest('Informations manquantes.');
|
||||
}
|
||||
|
||||
strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription)
|
||||
strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription);
|
||||
|
||||
const user = await strapi.documents('plugin::users-permissions.user').findOne({
|
||||
documentId: body.data.user.documentId
|
||||
})
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return ctx.notFound('Utilisateur introuvable.')
|
||||
return ctx.notFound('Utilisateur introuvable.');
|
||||
}
|
||||
|
||||
if (user.id !== data.user.id || user.username !== data.user.username || user.email !== data.user.email) {
|
||||
return ctx.badRequest('Informations non valides.')
|
||||
return ctx.badRequest('Informations non valides.');
|
||||
}
|
||||
|
||||
const artiste = await strapi.documents('api::artiste.artiste').findOne({
|
||||
documentId: data.artistes[0].documentId
|
||||
})
|
||||
});
|
||||
|
||||
if (!artiste) {
|
||||
return ctx.notFound('Artiste introuvable.')
|
||||
return ctx.notFound('Artiste introuvable.');
|
||||
}
|
||||
|
||||
const currentUserParole = await strapi.documents('api::parole.parole').findMany({
|
||||
@@ -108,22 +113,27 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({
|
||||
$eq: null
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
if (user && user.canAutoTranslate && data.traductionAuto && data.traductions.francais && (!data.traductions.anglais || !data.traductions.espagnol || !data.traductions.allemand || !data.traductions.italien)) {
|
||||
const translated = await strapi.service('api::parole.parole').translateLyrics(data.traductions.francais)
|
||||
data.traductions = translated
|
||||
const translated = await strapi.service('api::parole.parole').translateLyrics(data.traductions.francais);
|
||||
data.traductions = translated;
|
||||
}
|
||||
|
||||
const newParole = await strapi.documents('api::parole.parole').create({
|
||||
data: {
|
||||
...data
|
||||
titre: data.titre,
|
||||
transcription: data.transcription,
|
||||
traductions: data.traductions,
|
||||
traductionAuto: data.traductionAuto,
|
||||
artistes: [artiste.id],
|
||||
user: user.id
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const parolesIds = currentUserParole.map(({id}) => id)
|
||||
parolesIds.push(newParole.id)
|
||||
const parolesIds = currentUserParole.map(({id}) => id);
|
||||
parolesIds.push(newParole.id);
|
||||
|
||||
await strapi.documents('plugin::users-permissions.user').update({
|
||||
documentId: user.documentId,
|
||||
@@ -131,8 +141,8 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({
|
||||
data: {
|
||||
paroles: parolesIds
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return newParole
|
||||
return newParole;
|
||||
}
|
||||
}))
|
||||
}));
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import {describe, it, expect} from 'vitest'
|
||||
import isApiToken from '../is-api-token.js'
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import isApiToken from '../is-api-token.js';
|
||||
|
||||
describe('is-api-token policy', () => {
|
||||
it('autorise une requête authentifiée par token API', () => {
|
||||
const ctx = {state: {auth: {strategy: {name: 'api-token'}}}}
|
||||
expect(isApiToken(ctx)).toBe(true)
|
||||
})
|
||||
const ctx = {state: {auth: {strategy: {name: 'api-token'}}}};
|
||||
expect(isApiToken(ctx)).toBe(true);
|
||||
});
|
||||
|
||||
it('refuse une requête authentifiée autrement (ex: JWT utilisateur)', () => {
|
||||
const ctx = {state: {auth: {strategy: {name: 'users-permissions'}}}}
|
||||
expect(isApiToken(ctx)).toBe(false)
|
||||
})
|
||||
const ctx = {state: {auth: {strategy: {name: 'users-permissions'}}}};
|
||||
expect(isApiToken(ctx)).toBe(false);
|
||||
});
|
||||
|
||||
it('refuse une requête non authentifiée', () => {
|
||||
const ctx = {state: {}}
|
||||
expect(isApiToken(ctx)).toBe(false)
|
||||
})
|
||||
})
|
||||
const ctx = {state: {}};
|
||||
expect(isApiToken(ctx)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = policyContext => {
|
||||
return policyContext.state?.auth?.strategy?.name === 'api-token'
|
||||
}
|
||||
return policyContext.state?.auth?.strategy?.name === 'api-token';
|
||||
};
|
||||
|
||||
@@ -14,4 +14,4 @@ module.exports = createCoreRouter('api::parole.parole', {
|
||||
policies: [{name: 'global::is-document-owner', config: {uid: 'api::parole.parole'}}]
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
import {describe, it, expect, vi, afterEach} from 'vitest'
|
||||
import {describe, it, expect, vi, afterEach} from 'vitest';
|
||||
|
||||
const {default: createService} = await import('../parole.js')
|
||||
const {default: createService} = await import('../parole.js');
|
||||
|
||||
function fakeDeeplResponse(text) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({translations: [{text}]})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('Translator (DeepL)', () => {
|
||||
const originalFetch = global.fetch
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
global.fetch = originalFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('attache un timeout à la requête DeepL', async () => {
|
||||
global.fetch = vi.fn(async () => fakeDeeplResponse('hello'))
|
||||
global.fetch = vi.fn(async () => fakeDeeplResponse('hello'));
|
||||
|
||||
const strapi = {contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'}))}
|
||||
const service = createService({strapi})
|
||||
await service.translate('FR', 'EN', 'bonjour')
|
||||
const strapi = {contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'}))};
|
||||
const service = createService({strapi});
|
||||
await service.translate('FR', 'EN', 'bonjour');
|
||||
|
||||
const [, options] = global.fetch.mock.calls[0]
|
||||
expect(options.signal).toBeInstanceOf(AbortSignal)
|
||||
})
|
||||
})
|
||||
const [, options] = global.fetch.mock.calls[0];
|
||||
expect(options.signal).toBeInstanceOf(AbortSignal);
|
||||
});
|
||||
});
|
||||
|
||||
describe('translateLyrics', () => {
|
||||
const originalFetch = global.fetch
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
global.fetch = originalFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('continue les autres langues quand une traduction DeepL échoue', async () => {
|
||||
global.fetch = vi.fn(async (_url, options) => {
|
||||
const {target_lang: target} = JSON.parse(options.body)
|
||||
const {target_lang: target} = JSON.parse(options.body);
|
||||
if (target === 'ES') {
|
||||
return {ok: false, status: 500, text: async () => 'boom'}
|
||||
return {ok: false, status: 500, text: async () => 'boom'};
|
||||
}
|
||||
|
||||
return fakeDeeplResponse(`traduit-${target}`)
|
||||
})
|
||||
return fakeDeeplResponse(`traduit-${target}`);
|
||||
});
|
||||
|
||||
const strapi = {
|
||||
contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'})),
|
||||
log: {error: vi.fn()}
|
||||
}
|
||||
const service = createService({strapi})
|
||||
const result = await service.translateLyrics('Bonjour le monde')
|
||||
};
|
||||
const service = createService({strapi});
|
||||
const result = await service.translateLyrics('Bonjour le monde');
|
||||
|
||||
expect(result.anglais).toContain('traduit-EN')
|
||||
expect(result.espagnol).toBeUndefined()
|
||||
})
|
||||
})
|
||||
expect(result.anglais).toContain('traduit-EN');
|
||||
expect(result.espagnol).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
+101
-101
@@ -1,9 +1,8 @@
|
||||
'use strict';
|
||||
const qs = require('qs')
|
||||
const Diff = require('diff')
|
||||
const Diff = require('diff');
|
||||
|
||||
const { createCoreService } = require('@strapi/strapi').factories;
|
||||
const { ApplicationError } = require("@strapi/utils").errors
|
||||
const { ApplicationError } = require('@strapi/utils').errors;
|
||||
|
||||
const LANG_MAP = {
|
||||
fr: { field: 'francais', targetLang: 'fr', userPrompt: 'Tradui an fransé' },
|
||||
@@ -14,43 +13,43 @@ const LANG_MAP = {
|
||||
pt: { field: 'portugais', targetLang: 'pt', userPrompt: 'Traduza para o português', deeplTarget: 'PT-BR', suffix: '\n\n (Traduzido pela DeepL)' },
|
||||
ja: { field: 'japonais', targetLang: 'ja', userPrompt: '日本語に翻訳して', deeplTarget: 'JA', suffix: '\n\n (DeepLによる翻訳)' },
|
||||
ko: { field: 'coreen', targetLang: 'ko', userPrompt: '한국어로 번역해줘', deeplTarget: 'KO', suffix: '\n\n (DeepL 번역)' },
|
||||
}
|
||||
};
|
||||
|
||||
const ALL_LANGS = Object.keys(LANG_MAP)
|
||||
const ALL_LANGS = Object.keys(LANG_MAP);
|
||||
|
||||
function stripMarkdown(text) {
|
||||
if (!text) return ''
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/#{1,6}\s+/g, '')
|
||||
.replace(/\*\*(.*?)\*\*/gs, '$1')
|
||||
.replace(/\*(.*?)\*/gs, '$1')
|
||||
.replace(/__(.*?)__/gs, '$1')
|
||||
.replace(/_(.*?)_/gs, '$1')
|
||||
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
|
||||
.replace(/^[>\-\*\+]\s+/gm, '')
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
.replace(/^[>\-*+]\s+/gm, '')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Détecte si une transcription est probablement en français plutôt qu'en KA.
|
||||
// Heuristique : si les pronoms personnels français représentent > 4 % des mots.
|
||||
const FR_PRONOUNS = new Set(['je', 'tu', 'il', 'elle', 'nous', 'vous', 'ils', 'elles'])
|
||||
const FR_PRONOUNS = new Set(['je', 'tu', 'il', 'elle', 'nous', 'vous', 'ils', 'elles']);
|
||||
|
||||
function suspectFrench(text) {
|
||||
if (!text) return false
|
||||
const words = text.toLowerCase().match(/\b[a-zàâäéèêëîïôöùûüç]+\b/g) || []
|
||||
if (words.length < 10) return false
|
||||
const frCount = words.filter(w => FR_PRONOUNS.has(w)).length
|
||||
return frCount / words.length > 0.04
|
||||
if (!text) return false;
|
||||
const words = text.toLowerCase().match(/\b[a-zàâäéèêëîïôöùûüç]+\b/g) || [];
|
||||
if (words.length < 10) return false;
|
||||
const frCount = words.filter(w => FR_PRONOUNS.has(w)).length;
|
||||
return frCount / words.length > 0.04;
|
||||
}
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
class Translator {
|
||||
constructor() {
|
||||
this.deeplApi = process.env.DEEPL_URL || 'api-free.deepl.com'
|
||||
this.deeplKey = process.env.DEEPL_KEY
|
||||
this.urlRequest = `https://${this.deeplApi}/v2/translate`
|
||||
this.deeplApi = process.env.DEEPL_URL || 'api-free.deepl.com';
|
||||
this.deeplKey = process.env.DEEPL_KEY;
|
||||
this.urlRequest = `https://${this.deeplApi}/v2/translate`;
|
||||
}
|
||||
|
||||
async get(origin, target, text) {
|
||||
@@ -66,40 +65,40 @@ class Translator {
|
||||
target_lang: target,
|
||||
}),
|
||||
signal: AbortSignal.timeout(15_000)
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text()
|
||||
console.error('DeepL error:', body)
|
||||
throw new Error(`DeepL ${response.status}: ${body}`)
|
||||
const body = await response.text();
|
||||
console.error('DeepL error:', body);
|
||||
throw new Error(`DeepL ${response.status}: ${body}`);
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
const translator = new Translator()
|
||||
const translator = new Translator();
|
||||
|
||||
module.exports = createCoreService('api::parole.parole', ({strapi}) => ({
|
||||
async translate(origin, target, text) {
|
||||
const data = await translator.get(origin, target, text)
|
||||
return data.translations[0].text
|
||||
const data = await translator.get(origin, target, text);
|
||||
return data.translations[0].text;
|
||||
},
|
||||
async translateLyrics(parolesFR) {
|
||||
const result = { francais: parolesFR }
|
||||
const result = { francais: parolesFR };
|
||||
|
||||
for (const lang of ALL_LANGS) {
|
||||
if (lang === 'fr') continue
|
||||
const { field, deeplTarget, suffix } = LANG_MAP[lang]
|
||||
if (lang === 'fr') continue;
|
||||
const { field, deeplTarget, suffix } = LANG_MAP[lang];
|
||||
try {
|
||||
const translated = await this.translate('FR', deeplTarget, parolesFR)
|
||||
result[field] = translated + suffix
|
||||
const translated = await this.translate('FR', deeplTarget, parolesFR);
|
||||
result[field] = translated + suffix;
|
||||
} catch (err) {
|
||||
strapi.log.error(`DeepL (${deeplTarget}): ${err.message}`)
|
||||
strapi.log.error(`DeepL (${deeplTarget}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return result;
|
||||
},
|
||||
validateParoles(titre, transcription) {
|
||||
if (!titre || titre.trim().length === 0) {
|
||||
@@ -107,77 +106,77 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({
|
||||
}
|
||||
|
||||
if (!transcription || transcription.trim().length === 0) {
|
||||
throw new ApplicationError('Champ obligatoire. Veuillez renseigner la transcription.')
|
||||
throw new ApplicationError('Champ obligatoire. Veuillez renseigner la transcription.');
|
||||
}
|
||||
|
||||
if (transcription.trim().length < 10) {
|
||||
throw new ApplicationError('La transcription doit contenir au moins 10 caractères.')
|
||||
throw new ApplicationError('La transcription doit contenir au moins 10 caractères.');
|
||||
}
|
||||
},
|
||||
async fetchAllParoles() {
|
||||
const pageSize = 100
|
||||
let start = 0
|
||||
const all = []
|
||||
const pageSize = 100;
|
||||
let start = 0;
|
||||
const all = [];
|
||||
|
||||
while (true) {
|
||||
for (;;) {
|
||||
const batch = await strapi.documents('api::parole.parole').findMany({
|
||||
status: 'published',
|
||||
populate: ['artistes', 'traductions'],
|
||||
fields: ['documentId', 'titre', 'slug', 'transcription', 'annee', 'langueSource'],
|
||||
limit: pageSize,
|
||||
start,
|
||||
})
|
||||
all.push(...batch)
|
||||
if (batch.length < pageSize) break
|
||||
start += pageSize
|
||||
});
|
||||
all.push(...batch);
|
||||
if (batch.length < pageSize) break;
|
||||
start += pageSize;
|
||||
}
|
||||
|
||||
return all
|
||||
return all;
|
||||
},
|
||||
|
||||
buildExport(paroles, type, langs) {
|
||||
const targetLangs = langs && langs.length ? langs : ALL_LANGS
|
||||
const pairs = []
|
||||
const missing = []
|
||||
const nonKa = []
|
||||
const langCounts = {}
|
||||
const targetLangs = langs && langs.length ? langs : ALL_LANGS;
|
||||
const pairs = [];
|
||||
const missing = [];
|
||||
const nonKa = [];
|
||||
const langCounts = {};
|
||||
|
||||
for (const parole of paroles) {
|
||||
const source = stripMarkdown(parole.transcription)
|
||||
const sourceLang = parole.langueSource || 'ka'
|
||||
const artists = (parole.artistes || []).map(a => a.alias)
|
||||
const paroleMeta = { title: parole.titre, artists }
|
||||
const source = stripMarkdown(parole.transcription);
|
||||
const sourceLang = parole.langueSource || 'ka';
|
||||
const artists = (parole.artistes || []).map(a => a.alias);
|
||||
const paroleMeta = { title: parole.titre, artists };
|
||||
|
||||
if (sourceLang !== 'ka') {
|
||||
nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: sourceLang })
|
||||
nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: sourceLang });
|
||||
} else if (suspectFrench(source)) {
|
||||
nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: 'fr' })
|
||||
nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: 'fr' });
|
||||
}
|
||||
|
||||
const missingLangs = ALL_LANGS.filter(lang => !parole.traductions?.[LANG_MAP[lang].field])
|
||||
const missingLangs = ALL_LANGS.filter(lang => !parole.traductions?.[LANG_MAP[lang].field]);
|
||||
if (missingLangs.length > 0) {
|
||||
missing.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, missing: missingLangs })
|
||||
missing.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, missing: missingLangs });
|
||||
}
|
||||
|
||||
for (const lang of targetLangs) {
|
||||
const { field, targetLang, userPrompt } = LANG_MAP[lang]
|
||||
if (lang === sourceLang) continue
|
||||
const target = stripMarkdown(parole.traductions?.[field])
|
||||
if (!target) continue
|
||||
const { field, targetLang, userPrompt } = LANG_MAP[lang];
|
||||
if (lang === sourceLang) continue;
|
||||
const target = stripMarkdown(parole.traductions?.[field]);
|
||||
if (!target) continue;
|
||||
|
||||
langCounts[lang] = (langCounts[lang] || 0) + 1
|
||||
langCounts[lang] = (langCounts[lang] || 0) + 1;
|
||||
|
||||
if (type === 'instruct') {
|
||||
const systemPrompt = sourceLang === 'ka'
|
||||
? 'Tu es un expert en langue KA (créole guadeloupéen/martiniquais). Traduis le texte KA suivant.'
|
||||
: `Tu es un expert en traduction. Traduis le texte suivant (langue source : ${sourceLang}).`
|
||||
: `Tu es un expert en traduction. Traduis le texte suivant (langue source : ${sourceLang}).`;
|
||||
pairs.push({
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: `${userPrompt} :\n\n${source}` },
|
||||
{ role: 'assistant', content: target },
|
||||
],
|
||||
})
|
||||
});
|
||||
} else {
|
||||
pairs.push({
|
||||
source_lang: sourceLang,
|
||||
@@ -185,7 +184,7 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({
|
||||
source,
|
||||
target,
|
||||
...paroleMeta,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,60 +196,61 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({
|
||||
languages: langCounts,
|
||||
missing_translations: missing,
|
||||
non_ka_transcriptions: nonKa,
|
||||
}
|
||||
};
|
||||
|
||||
return { metadata, pairs }
|
||||
return { metadata, pairs };
|
||||
},
|
||||
|
||||
async bulkTranslateMissing() {
|
||||
const TARGET_LANGS = ALL_LANGS
|
||||
.filter(lang => lang !== 'fr')
|
||||
.map(lang => ({ lang, ...LANG_MAP[lang] }))
|
||||
.map(lang => ({ lang, ...LANG_MAP[lang] }));
|
||||
|
||||
const pageSize = 100
|
||||
let start = 0
|
||||
const all = []
|
||||
while (true) {
|
||||
const pageSize = 100;
|
||||
let start = 0;
|
||||
const all = [];
|
||||
for (;;) {
|
||||
const batch = await strapi.documents('api::parole.parole').findMany({
|
||||
status: 'published',
|
||||
populate: ['traductions'],
|
||||
fields: ['documentId', 'slug', 'titre', 'transcription', 'langueSource'],
|
||||
limit: pageSize,
|
||||
start,
|
||||
})
|
||||
all.push(...batch)
|
||||
if (batch.length < pageSize) break
|
||||
start += pageSize
|
||||
});
|
||||
all.push(...batch);
|
||||
if (batch.length < pageSize) break;
|
||||
start += pageSize;
|
||||
}
|
||||
|
||||
const translated = []
|
||||
const skipped = []
|
||||
const errors = []
|
||||
const translated = [];
|
||||
const skipped = [];
|
||||
const errors = [];
|
||||
|
||||
for (const parole of all) {
|
||||
const sourceFR = parole.traductions?.francais
|
||||
|| (parole.langueSource === 'fr' ? parole.transcription : null)
|
||||
|| (parole.langueSource === 'fr' ? parole.transcription : null);
|
||||
|
||||
if (!sourceFR) { skipped.push(parole.slug); continue }
|
||||
if (!sourceFR) { skipped.push(parole.slug); continue; }
|
||||
|
||||
const missing = TARGET_LANGS.filter(({ field }) => !parole.traductions?.[field])
|
||||
if (missing.length === 0) { skipped.push(parole.slug); continue }
|
||||
const missing = TARGET_LANGS.filter(({ field }) => !parole.traductions?.[field]);
|
||||
if (missing.length === 0) { skipped.push(parole.slug); continue; }
|
||||
|
||||
const { id: _id, ...tradData } = parole.traductions || {}
|
||||
const updatedTrad = { ...tradData }
|
||||
const addedLangs = []
|
||||
// eslint-disable-next-line no-unused-vars -- id exclu intentionnellement du spread
|
||||
const { id: _id, ...tradData } = parole.traductions || {};
|
||||
const updatedTrad = { ...tradData };
|
||||
const addedLangs = [];
|
||||
|
||||
for (const { lang, field, deeplTarget, suffix } of missing) {
|
||||
try {
|
||||
await sleep(700)
|
||||
const result = await translator.get('FR', deeplTarget, sourceFR)
|
||||
const text = result?.translations?.[0]?.text
|
||||
await sleep(700);
|
||||
const result = await translator.get('FR', deeplTarget, sourceFR);
|
||||
const text = result?.translations?.[0]?.text;
|
||||
if (text) {
|
||||
updatedTrad[field] = text + suffix
|
||||
addedLangs.push(lang)
|
||||
updatedTrad[field] = text + suffix;
|
||||
addedLangs.push(lang);
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push({ slug: parole.slug, lang: deeplTarget, error: err.message })
|
||||
errors.push({ slug: parole.slug, lang: deeplTarget, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,28 +258,28 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({
|
||||
await strapi.documents('api::parole.parole').update({
|
||||
documentId: parole.documentId,
|
||||
data: { traductions: updatedTrad },
|
||||
})
|
||||
});
|
||||
await strapi.documents('api::parole.parole').publish({
|
||||
documentId: parole.documentId,
|
||||
})
|
||||
translated.push({ slug: parole.slug, langs: addedLangs })
|
||||
});
|
||||
translated.push({ slug: parole.slug, langs: addedLangs });
|
||||
}
|
||||
}
|
||||
|
||||
return { translated, skipped, errors }
|
||||
return { translated, skipped, errors };
|
||||
},
|
||||
|
||||
parolesDiff(titre = '', oldString, newString) {
|
||||
const patch = Diff.createPatch(titre, oldString, newString, 'supprimée', 'ajoutée')
|
||||
const parsePatch = Diff.parsePatch(patch)
|
||||
const patch = Diff.createPatch(titre, oldString, newString, 'supprimée', 'ajoutée');
|
||||
const parsePatch = Diff.parsePatch(patch);
|
||||
|
||||
if (parsePatch[0].hunks.length > 0) {
|
||||
const jsonDiff = Diff.diffWords(oldString, newString)
|
||||
const jsonDiff = Diff.diffWords(oldString, newString);
|
||||
|
||||
return {
|
||||
patch,
|
||||
jsonDiff
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async count(ctx, next) {
|
||||
async count() {
|
||||
const countArtiste = await strapi.documents('api::artiste.artiste').count({
|
||||
publicationState: 'live'
|
||||
})
|
||||
});
|
||||
|
||||
const countParole = await strapi.documents('api::parole.parole').count({
|
||||
publicationState: 'live'
|
||||
})
|
||||
});
|
||||
|
||||
return {countArtiste, countParole}
|
||||
return {countArtiste, countParole};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,4 +9,4 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
import {describe, it, expect, vi} from 'vitest'
|
||||
import {describe, it, expect, vi} from 'vitest';
|
||||
|
||||
const {default: isDocumentOwner} = await import('../is-document-owner.js')
|
||||
const {default: isDocumentOwner} = await import('../is-document-owner.js');
|
||||
|
||||
function buildStrapi({jwtUserId, document}) {
|
||||
const dbQuery = {
|
||||
findOne: vi.fn(async () => document)
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
plugins: {
|
||||
@@ -21,7 +21,7 @@ function buildStrapi({jwtUserId, document}) {
|
||||
query: vi.fn(() => dbQuery)
|
||||
},
|
||||
dbQuery
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildPolicyContext({authorization = 'Bearer faketoken', paramId, bodyDocumentId} = {}) {
|
||||
@@ -31,43 +31,43 @@ function buildPolicyContext({authorization = 'Bearer faketoken', paramId, bodyDo
|
||||
header: authorization ? {authorization} : {},
|
||||
body: {data: {documentId: bodyDocumentId}}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('is-document-owner policy', () => {
|
||||
it("refuse quand aucun en-tête d'autorisation n'est présent", async () => {
|
||||
const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}})
|
||||
const policyContext = buildPolicyContext({authorization: null, paramId: 'doc-1'})
|
||||
it('refuse quand aucun en-tête d\'autorisation n\'est présent', async () => {
|
||||
const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}});
|
||||
const policyContext = buildPolicyContext({authorization: null, paramId: 'doc-1'});
|
||||
|
||||
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée')
|
||||
})
|
||||
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée');
|
||||
});
|
||||
|
||||
it('autorise quand le user du JWT est le propriétaire du document (id dans les params)', async () => {
|
||||
const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}})
|
||||
const policyContext = buildPolicyContext({paramId: 'doc-1'})
|
||||
const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}});
|
||||
const policyContext = buildPolicyContext({paramId: 'doc-1'});
|
||||
|
||||
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true)
|
||||
expect(strapi.dbQuery.findOne).toHaveBeenCalledWith({where: {documentId: 'doc-1'}, populate: {user: true}})
|
||||
})
|
||||
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true);
|
||||
expect(strapi.dbQuery.findOne).toHaveBeenCalledWith({where: {documentId: 'doc-1'}, populate: {user: true}});
|
||||
});
|
||||
|
||||
it('autorise quand le documentId vient du corps de la requête (cas du contrôleur parole.update)', async () => {
|
||||
const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}})
|
||||
const policyContext = buildPolicyContext({bodyDocumentId: 'doc-1'})
|
||||
const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}});
|
||||
const policyContext = buildPolicyContext({bodyDocumentId: 'doc-1'});
|
||||
|
||||
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true)
|
||||
})
|
||||
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("refuse quand le user du JWT n'est pas le propriétaire du document", async () => {
|
||||
const strapi = buildStrapi({jwtUserId: 999, document: {documentId: 'doc-1', user: {id: 1}}})
|
||||
const policyContext = buildPolicyContext({paramId: 'doc-1'})
|
||||
it('refuse quand le user du JWT n\'est pas le propriétaire du document', async () => {
|
||||
const strapi = buildStrapi({jwtUserId: 999, document: {documentId: 'doc-1', user: {id: 1}}});
|
||||
const policyContext = buildPolicyContext({paramId: 'doc-1'});
|
||||
|
||||
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée')
|
||||
})
|
||||
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée');
|
||||
});
|
||||
|
||||
it("refuse quand le document ciblé n'existe pas", async () => {
|
||||
const strapi = buildStrapi({jwtUserId: 1, document: null})
|
||||
const policyContext = buildPolicyContext({paramId: 'doc-inconnu'})
|
||||
it('refuse quand le document ciblé n\'existe pas', async () => {
|
||||
const strapi = buildStrapi({jwtUserId: 1, document: null});
|
||||
const policyContext = buildPolicyContext({paramId: 'doc-inconnu'});
|
||||
|
||||
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Ressource introuvable.')
|
||||
})
|
||||
})
|
||||
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Ressource introuvable.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {describe, it, expect, vi} from 'vitest'
|
||||
import {describe, it, expect, vi} from 'vitest';
|
||||
|
||||
const {default: isPayloadOwner} = await import('../is-payload-owner.js')
|
||||
const {default: isPayloadOwner} = await import('../is-payload-owner.js');
|
||||
|
||||
function buildStrapi(jwtUserId) {
|
||||
return {
|
||||
@@ -13,7 +13,7 @@ function buildStrapi(jwtUserId) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildPolicyContext({authorization, payloadUserId}) {
|
||||
@@ -22,30 +22,30 @@ function buildPolicyContext({authorization, payloadUserId}) {
|
||||
header: authorization ? {authorization} : {},
|
||||
body: {data: {user: {id: payloadUserId}}}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('is-payload-owner policy', () => {
|
||||
it("refuse quand aucun en-tête d'autorisation n'est présent", async () => {
|
||||
const strapi = buildStrapi(999)
|
||||
const policyContext = buildPolicyContext({authorization: undefined, payloadUserId: 1})
|
||||
it('refuse quand aucun en-tête d\'autorisation n\'est présent', async () => {
|
||||
const strapi = buildStrapi(999);
|
||||
const policyContext = buildPolicyContext({authorization: undefined, payloadUserId: 1});
|
||||
|
||||
await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée')
|
||||
})
|
||||
await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée');
|
||||
});
|
||||
|
||||
it('autorise quand le user du JWT correspond au user du payload', async () => {
|
||||
const strapi = buildStrapi(1)
|
||||
const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1})
|
||||
const strapi = buildStrapi(1);
|
||||
const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1});
|
||||
|
||||
await expect(isPayloadOwner(policyContext, {}, {strapi})).resolves.toBe(true)
|
||||
})
|
||||
await expect(isPayloadOwner(policyContext, {}, {strapi})).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('refuse quand le user du JWT ne correspond pas au user du payload', async () => {
|
||||
const strapi = buildStrapi(999)
|
||||
const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1})
|
||||
const strapi = buildStrapi(999);
|
||||
const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1});
|
||||
|
||||
await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée')
|
||||
})
|
||||
await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée');
|
||||
});
|
||||
|
||||
it('refuse quand le token est invalide', async () => {
|
||||
const strapi = {
|
||||
@@ -54,15 +54,15 @@ describe('is-payload-owner policy', () => {
|
||||
services: {
|
||||
jwt: {
|
||||
getToken: vi.fn(async () => {
|
||||
throw new Error('Invalid token.')
|
||||
throw new Error('Invalid token.');
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1})
|
||||
};
|
||||
const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1});
|
||||
|
||||
await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée')
|
||||
})
|
||||
})
|
||||
await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
const { UnauthorizedError, NotFoundError } = require('@strapi/utils').errors
|
||||
const { UnauthorizedError, NotFoundError } = require('@strapi/utils').errors;
|
||||
|
||||
module.exports = async (policyContext, config, {strapi}) => {
|
||||
const {request, params} = policyContext
|
||||
const {request, params} = policyContext;
|
||||
|
||||
if (!request?.header?.authorization) {
|
||||
throw new UnauthorizedError('Opération non autorisée')
|
||||
throw new UnauthorizedError('Opération non autorisée');
|
||||
}
|
||||
|
||||
let jwtUserId
|
||||
let jwtUserId;
|
||||
try {
|
||||
({id: jwtUserId} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext))
|
||||
({id: jwtUserId} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext));
|
||||
} catch (err) {
|
||||
throw new UnauthorizedError('Opération non autorisée')
|
||||
throw new UnauthorizedError('Opération non autorisée');
|
||||
}
|
||||
|
||||
const documentId = params?.id ?? request.body?.data?.documentId
|
||||
const documentId = params?.id ?? request.body?.data?.documentId;
|
||||
|
||||
const document = await strapi.db.query(config.uid).findOne({
|
||||
where: {documentId},
|
||||
populate: {user: true}
|
||||
})
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new NotFoundError('Ressource introuvable.')
|
||||
throw new NotFoundError('Ressource introuvable.');
|
||||
}
|
||||
|
||||
if (document.user?.id !== jwtUserId) {
|
||||
throw new UnauthorizedError('Opération non autorisée')
|
||||
throw new UnauthorizedError('Opération non autorisée');
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const { UnauthorizedError } = require('@strapi/utils').errors
|
||||
const { UnauthorizedError } = require('@strapi/utils').errors;
|
||||
|
||||
module.exports = async (policyContext, config, {strapi}) => {
|
||||
const {request} = policyContext
|
||||
const {request} = policyContext;
|
||||
|
||||
if (!request?.header?.authorization) {
|
||||
throw new UnauthorizedError('Opération non autorisée')
|
||||
throw new UnauthorizedError('Opération non autorisée');
|
||||
}
|
||||
|
||||
try {
|
||||
const {id} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext)
|
||||
const {id} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext);
|
||||
|
||||
if (id !== request.body?.data?.user?.id) {
|
||||
throw new UnauthorizedError('Opération non autorisée')
|
||||
throw new UnauthorizedError('Opération non autorisée');
|
||||
}
|
||||
} catch (err) {
|
||||
throw new UnauthorizedError('Opération non autorisée')
|
||||
throw new UnauthorizedError('Opération non autorisée');
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
import {describe, it, expect, afterEach} from 'vitest'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import {backupDatabase} from '../backup-database.js'
|
||||
import {describe, it, expect, afterEach} from 'vitest';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import {backupDatabase} from '../backup-database.js';
|
||||
|
||||
const tmpDirs = []
|
||||
const tmpDirs = [];
|
||||
|
||||
function makeTmpDir() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'backup-database-test-'))
|
||||
tmpDirs.push(dir)
|
||||
return dir
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'backup-database-test-'));
|
||||
tmpDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
describe('backupDatabase', () => {
|
||||
afterEach(() => {
|
||||
for (const dir of tmpDirs.splice(0)) {
|
||||
fs.rmSync(dir, {recursive: true, force: true})
|
||||
fs.rmSync(dir, {recursive: true, force: true});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it("ne fait rien quand le fichier de base n'existe pas", () => {
|
||||
const root = makeTmpDir()
|
||||
it('ne fait rien quand le fichier de base n\'existe pas', () => {
|
||||
const root = makeTmpDir();
|
||||
const result = backupDatabase({
|
||||
dbPath: path.join(root, 'inexistant.db'),
|
||||
backupsDir: path.join(root, 'backups')
|
||||
})
|
||||
});
|
||||
|
||||
expect(result).toBeNull()
|
||||
expect(fs.existsSync(path.join(root, 'backups'))).toBe(false)
|
||||
})
|
||||
expect(result).toBeNull();
|
||||
expect(fs.existsSync(path.join(root, 'backups'))).toBe(false);
|
||||
});
|
||||
|
||||
it('copie le fichier de base dans le dossier de sauvegardes', () => {
|
||||
const root = makeTmpDir()
|
||||
const dbPath = path.join(root, 'data.db')
|
||||
fs.writeFileSync(dbPath, 'contenu-de-la-base')
|
||||
const backupsDir = path.join(root, 'backups')
|
||||
const root = makeTmpDir();
|
||||
const dbPath = path.join(root, 'data.db');
|
||||
fs.writeFileSync(dbPath, 'contenu-de-la-base');
|
||||
const backupsDir = path.join(root, 'backups');
|
||||
|
||||
const result = backupDatabase({dbPath, backupsDir})
|
||||
const result = backupDatabase({dbPath, backupsDir});
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(fs.existsSync(result)).toBe(true)
|
||||
expect(fs.readFileSync(result, 'utf8')).toBe('contenu-de-la-base')
|
||||
})
|
||||
expect(result).not.toBeNull();
|
||||
expect(fs.existsSync(result)).toBe(true);
|
||||
expect(fs.readFileSync(result, 'utf8')).toBe('contenu-de-la-base');
|
||||
});
|
||||
|
||||
it('ne conserve que les 8 sauvegardes les plus récentes', () => {
|
||||
const root = makeTmpDir()
|
||||
const dbPath = path.join(root, 'data.db')
|
||||
fs.writeFileSync(dbPath, 'contenu')
|
||||
const backupsDir = path.join(root, 'backups')
|
||||
fs.mkdirSync(backupsDir, {recursive: true})
|
||||
const root = makeTmpDir();
|
||||
const dbPath = path.join(root, 'data.db');
|
||||
fs.writeFileSync(dbPath, 'contenu');
|
||||
const backupsDir = path.join(root, 'backups');
|
||||
fs.mkdirSync(backupsDir, {recursive: true});
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
fs.writeFileSync(path.join(backupsDir, `data-2020-01-0${i}.db`), 'ancien')
|
||||
fs.writeFileSync(path.join(backupsDir, `data-2020-01-0${i}.db`), 'ancien');
|
||||
}
|
||||
|
||||
backupDatabase({dbPath, backupsDir})
|
||||
backupDatabase({dbPath, backupsDir});
|
||||
|
||||
const remaining = fs.readdirSync(backupsDir).filter(name => name.startsWith('data-'))
|
||||
expect(remaining).toHaveLength(8)
|
||||
})
|
||||
})
|
||||
const remaining = fs.readdirSync(backupsDir).filter(name => name.startsWith('data-'));
|
||||
expect(remaining).toHaveLength(8);
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+3
-4
@@ -521,6 +521,7 @@ export interface ApiParoleParole extends Struct.CollectionTypeSchema {
|
||||
};
|
||||
options: {
|
||||
draftAndPublish: true;
|
||||
populateCreatorFields: true;
|
||||
};
|
||||
attributes: {
|
||||
annee: Schema.Attribute.Integer;
|
||||
@@ -531,8 +532,7 @@ export interface ApiParoleParole extends Struct.CollectionTypeSchema {
|
||||
>;
|
||||
couverture: Schema.Attribute.Media<'images'>;
|
||||
createdAt: Schema.Attribute.DateTime;
|
||||
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||
Schema.Attribute.Private;
|
||||
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'>;
|
||||
creativeCommons: Schema.Attribute.Enumeration<
|
||||
['BY', 'BY-SA', 'BY-ND', 'BY-NC', 'BY-NC-SA', 'BY-NC-ND']
|
||||
>;
|
||||
@@ -576,8 +576,7 @@ export interface ApiParoleParole extends Struct.CollectionTypeSchema {
|
||||
traductions: Schema.Attribute.Component<'trad.traductions', false>;
|
||||
transcription: Schema.Attribute.RichText & Schema.Attribute.Required;
|
||||
updatedAt: Schema.Attribute.DateTime;
|
||||
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||
Schema.Attribute.Private;
|
||||
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'>;
|
||||
user: Schema.Attribute.Relation<
|
||||
'oneToOne',
|
||||
'plugin::users-permissions.user'
|
||||
|
||||
@@ -1036,6 +1036,38 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz#8fe30b3088b89b4873c3a6cc87597ae3920c0a8b"
|
||||
integrity sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0":
|
||||
version "4.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
|
||||
integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@eslint-community/regexpp@^4.6.1":
|
||||
version "4.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
|
||||
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
|
||||
|
||||
"@eslint/eslintrc@^2.1.4":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad"
|
||||
integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
|
||||
dependencies:
|
||||
ajv "^6.12.4"
|
||||
debug "^4.3.2"
|
||||
espree "^9.6.0"
|
||||
globals "^13.19.0"
|
||||
ignore "^5.2.0"
|
||||
import-fresh "^3.2.1"
|
||||
js-yaml "^4.1.0"
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@8.57.1":
|
||||
version "8.57.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
|
||||
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
|
||||
|
||||
"@floating-ui/core@^1.0.5":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.1.0.tgz#0a1dee4bbce87ff71602625d33f711cafd8afc08"
|
||||
@@ -1169,6 +1201,25 @@
|
||||
resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-3.0.0.tgz#f11fdf7dda62fe8e336fa7c6642d9041f30356d7"
|
||||
integrity sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==
|
||||
|
||||
"@humanwhocodes/config-array@^0.13.0":
|
||||
version "0.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748"
|
||||
integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==
|
||||
dependencies:
|
||||
"@humanwhocodes/object-schema" "^2.0.3"
|
||||
debug "^4.3.1"
|
||||
minimatch "^3.0.5"
|
||||
|
||||
"@humanwhocodes/module-importer@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
|
||||
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
|
||||
|
||||
"@humanwhocodes/object-schema@^2.0.3":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
|
||||
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
|
||||
|
||||
"@img/sharp-darwin-arm64@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08"
|
||||
@@ -1507,7 +1558,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
|
||||
"@nodelib/fs.walk@^1.2.3":
|
||||
"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
|
||||
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
|
||||
@@ -3818,6 +3869,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
|
||||
integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
|
||||
|
||||
"@ungap/structured-clone@^1.2.0":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.2.tgz#a03ad82cd5676414d068ba86f880c5681194aadf"
|
||||
integrity sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==
|
||||
|
||||
"@vercel/oidc@3.0.5":
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@vercel/oidc/-/oidc-3.0.5.tgz#bd8db7ee777255c686443413492db4d98ef49657"
|
||||
@@ -4044,6 +4100,11 @@ acorn-import-phases@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7"
|
||||
integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==
|
||||
|
||||
acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn-walk@^8.0.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f"
|
||||
@@ -4064,6 +4125,11 @@ acorn@^8.5.0:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
|
||||
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
|
||||
|
||||
acorn@^8.9.0:
|
||||
version "8.17.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe"
|
||||
integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==
|
||||
|
||||
addressparser@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746"
|
||||
@@ -4126,6 +4192,16 @@ ajv@8.18.0, ajv@^8.9.0:
|
||||
json-schema-traverse "^1.0.0"
|
||||
require-from-string "^2.0.2"
|
||||
|
||||
ajv@^6.12.4:
|
||||
version "6.15.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.15.0.tgz#07e982c74626167aa7a2495c53817892d7139492"
|
||||
integrity sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.1"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ajv@^6.12.5:
|
||||
version "6.12.6"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||
@@ -4659,7 +4735,7 @@ chai@^6.2.2:
|
||||
resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e"
|
||||
integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==
|
||||
|
||||
chalk@4.1.2, chalk@^4.1.0, chalk@^4.1.2:
|
||||
chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
@@ -5154,6 +5230,15 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3:
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
cross-spawn@^7.0.2:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
|
||||
dependencies:
|
||||
path-key "^3.1.0"
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
crypto-random-string@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
|
||||
@@ -5304,6 +5389,11 @@ deep-extend@^0.6.0:
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||
|
||||
deep-is@^0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
||||
|
||||
deepmerge@^2.1.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
|
||||
@@ -5450,6 +5540,13 @@ dnd-core@^16.0.1:
|
||||
"@react-dnd/invariant" "^4.0.1"
|
||||
redux "^4.2.0"
|
||||
|
||||
doctrine@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
|
||||
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
dom-accessibility-api@^0.5.9:
|
||||
version "0.5.16"
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
||||
@@ -5851,16 +5948,89 @@ eslint-scope@5.1.1:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-scope@^7.2.2:
|
||||
version "7.2.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
|
||||
integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
|
||||
dependencies:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^5.2.0"
|
||||
|
||||
eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
|
||||
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
|
||||
|
||||
eslint@^8.7.0:
|
||||
version "8.57.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9"
|
||||
integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@eslint-community/regexpp" "^4.6.1"
|
||||
"@eslint/eslintrc" "^2.1.4"
|
||||
"@eslint/js" "8.57.1"
|
||||
"@humanwhocodes/config-array" "^0.13.0"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
"@nodelib/fs.walk" "^1.2.8"
|
||||
"@ungap/structured-clone" "^1.2.0"
|
||||
ajv "^6.12.4"
|
||||
chalk "^4.0.0"
|
||||
cross-spawn "^7.0.2"
|
||||
debug "^4.3.2"
|
||||
doctrine "^3.0.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
eslint-scope "^7.2.2"
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
espree "^9.6.1"
|
||||
esquery "^1.4.2"
|
||||
esutils "^2.0.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
file-entry-cache "^6.0.1"
|
||||
find-up "^5.0.0"
|
||||
glob-parent "^6.0.2"
|
||||
globals "^13.19.0"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.2.0"
|
||||
imurmurhash "^0.1.4"
|
||||
is-glob "^4.0.0"
|
||||
is-path-inside "^3.0.3"
|
||||
js-yaml "^4.1.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
levn "^0.4.1"
|
||||
lodash.merge "^4.6.2"
|
||||
minimatch "^3.1.2"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.9.3"
|
||||
strip-ansi "^6.0.1"
|
||||
text-table "^0.2.0"
|
||||
|
||||
esm@^3.2.25:
|
||||
version "3.2.25"
|
||||
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
|
||||
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
|
||||
|
||||
espree@^9.6.0, espree@^9.6.1:
|
||||
version "9.6.1"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
|
||||
integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
|
||||
dependencies:
|
||||
acorn "^8.9.0"
|
||||
acorn-jsx "^5.3.2"
|
||||
eslint-visitor-keys "^3.4.1"
|
||||
|
||||
esprima@^4.0.0, esprima@~4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||
|
||||
esquery@^1.4.2:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d"
|
||||
integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==
|
||||
dependencies:
|
||||
estraverse "^5.1.0"
|
||||
|
||||
esrecurse@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
|
||||
@@ -5873,6 +6043,11 @@ estraverse@^4.1.1:
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
|
||||
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
|
||||
|
||||
estraverse@^5.1.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
|
||||
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
|
||||
|
||||
estraverse@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
|
||||
@@ -5890,6 +6065,11 @@ estree-walker@^3.0.3:
|
||||
dependencies:
|
||||
"@types/estree" "^1.0.0"
|
||||
|
||||
esutils@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||
|
||||
events@^3.2.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||
@@ -5963,6 +6143,11 @@ fast-json-stable-stringify@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||
|
||||
fast-levenshtein@^2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||
|
||||
fast-safe-stringify@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
||||
@@ -5990,6 +6175,13 @@ fecha@^4.2.0:
|
||||
resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd"
|
||||
integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==
|
||||
|
||||
file-entry-cache@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
|
||||
integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
|
||||
dependencies:
|
||||
flat-cache "^3.0.4"
|
||||
|
||||
file-selector@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-2.1.2.tgz#fe7c7ee9e550952dfbc863d73b14dc740d7de8b4"
|
||||
@@ -6097,6 +6289,20 @@ flagged-respawn@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-2.0.0.tgz#abf39719dcfe1ac06c86c9466081c541c682987b"
|
||||
integrity sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==
|
||||
|
||||
flat-cache@^3.0.4:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee"
|
||||
integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
|
||||
dependencies:
|
||||
flatted "^3.2.9"
|
||||
keyv "^4.5.3"
|
||||
rimraf "^3.0.2"
|
||||
|
||||
flatted@^3.2.9:
|
||||
version "3.4.2"
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726"
|
||||
integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==
|
||||
|
||||
flow-parser@0.*:
|
||||
version "0.309.0"
|
||||
resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.309.0.tgz#ca2eae0b1a604cafbba99863785a92f7164671ee"
|
||||
@@ -6406,6 +6612,13 @@ glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob-parent@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
|
||||
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
|
||||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob-to-regexp@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
|
||||
@@ -6462,6 +6675,13 @@ global-prefix@^1.0.1:
|
||||
is-windows "^1.0.1"
|
||||
which "^1.2.14"
|
||||
|
||||
globals@^13.19.0:
|
||||
version "13.24.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
|
||||
integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
|
||||
dependencies:
|
||||
type-fest "^0.20.2"
|
||||
|
||||
globalthis@^1.0.2:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236"
|
||||
@@ -6521,6 +6741,11 @@ grant@5.4.24:
|
||||
jwk-to-pem "^2.0.7"
|
||||
jws "^4.0.0"
|
||||
|
||||
graphemer@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
||||
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
||||
|
||||
gzip-size@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
|
||||
@@ -6868,6 +7093,11 @@ ignore-by-default@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
||||
integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
|
||||
|
||||
ignore@^5.2.0:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
|
||||
|
||||
immediate@~3.0.5:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
@@ -7088,6 +7318,13 @@ is-generator-function@^1.0.7:
|
||||
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.8.tgz#dfb5c2b120e02b0a8d9d2c6806cd5621aa922f7b"
|
||||
integrity sha512-2Omr/twNtufVZFr1GhxjOMFPAj2sjc/dKaIqBhvo4qciXfJmITGH6ZGd8eZYNHza8t1y0e01AuqRhJwfWp26WQ==
|
||||
|
||||
is-glob@^4.0.0, is-glob@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||
dependencies:
|
||||
is-extglob "^2.1.1"
|
||||
|
||||
is-glob@^4.0.1, is-glob@~4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
|
||||
@@ -7095,13 +7332,6 @@ is-glob@^4.0.1, is-glob@~4.0.1:
|
||||
dependencies:
|
||||
is-extglob "^2.1.1"
|
||||
|
||||
is-glob@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||
dependencies:
|
||||
is-extglob "^2.1.1"
|
||||
|
||||
is-hexadecimal@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027"
|
||||
@@ -7132,6 +7362,11 @@ is-obj@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
|
||||
integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
|
||||
|
||||
is-path-inside@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
|
||||
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
|
||||
|
||||
is-plain-obj@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
|
||||
@@ -7268,6 +7503,13 @@ js-yaml@^3.13.0:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
js-yaml@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.0.tgz#d1900572a7f7cf0b5f540c83673e60bad3436592"
|
||||
integrity sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
jscodeshift@17.3.0:
|
||||
version "17.3.0"
|
||||
resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-17.3.0.tgz#b9ea1d8d1c9255103bfc4cb42ddb46e18cb2415c"
|
||||
@@ -7327,6 +7569,11 @@ json-schema@^0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
|
||||
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
|
||||
|
||||
json-stable-stringify-without-jsonify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
||||
|
||||
json5@^2.1.2:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
|
||||
@@ -7443,6 +7690,13 @@ keyv@^4.0.0:
|
||||
compress-brotli "^1.3.8"
|
||||
json-buffer "3.0.1"
|
||||
|
||||
keyv@^4.5.3:
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
|
||||
integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
|
||||
dependencies:
|
||||
json-buffer "3.0.1"
|
||||
|
||||
kind-of@^6.0.2:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||
@@ -7611,6 +7865,14 @@ kuler@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
|
||||
integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
|
||||
|
||||
levn@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
|
||||
integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
|
||||
dependencies:
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
libbase64@0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-0.1.0.tgz#62351a839563ac5ff5bd26f12f60e9830bb751e6"
|
||||
@@ -7816,6 +8078,11 @@ lodash.isplainobject@4.0.6:
|
||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
|
||||
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
lodash.sortby@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
@@ -8526,6 +8793,13 @@ minimatch@^3.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^3.0.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
|
||||
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
@@ -8673,6 +8947,11 @@ napi-build-utils@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
|
||||
integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
negotiator@0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||
@@ -8916,6 +9195,18 @@ opener@^1.5.2:
|
||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
||||
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
|
||||
|
||||
optionator@^0.9.3:
|
||||
version "0.9.4"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
|
||||
integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==
|
||||
dependencies:
|
||||
deep-is "^0.1.3"
|
||||
fast-levenshtein "^2.0.6"
|
||||
levn "^0.4.1"
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "^0.4.0"
|
||||
word-wrap "^1.2.5"
|
||||
|
||||
ora@5.4.1, ora@^5.4.1:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18"
|
||||
@@ -9401,6 +9692,11 @@ preferred-pm@3.1.3:
|
||||
path-exists "^4.0.0"
|
||||
which-pm "2.0.0"
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier@3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
|
||||
@@ -10133,7 +10429,7 @@ reusify@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
rimraf@3.0.2:
|
||||
rimraf@3.0.2, 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==
|
||||
@@ -10826,6 +11122,11 @@ strip-final-newline@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
|
||||
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
|
||||
|
||||
strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
strip-json-comments@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
@@ -11009,6 +11310,11 @@ text-hex@1.0.x:
|
||||
resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
|
||||
integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
|
||||
|
||||
text-table@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
|
||||
|
||||
thenify-all@^1.0.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
|
||||
@@ -11214,6 +11520,13 @@ tunnel-agent@^0.6.0:
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||
integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
|
||||
dependencies:
|
||||
prelude-ls "^1.2.1"
|
||||
|
||||
type-fest@^0.20.2:
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
||||
@@ -11751,6 +12064,11 @@ winston@3.10.0:
|
||||
triple-beam "^1.3.0"
|
||||
winston-transport "^4.5.0"
|
||||
|
||||
word-wrap@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
wordwrap@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
|
||||
Reference in New Issue
Block a user