caencamp / jobs-caen-camp Goto Github PK
View Code? Open in Web Editor NEWGestion d'offres d'emploi pour les CaenCamp
Home Page: https://www.caen.camp
License: GNU General Public License v3.0
Gestion d'offres d'emploi pour les CaenCamp
Home Page: https://www.caen.camp
License: GNU General Public License v3.0
Afin de chacun puisse commencer à participer au projet :
Nous avons mis en place une administration basée sur react-admin. Avant de pouvoir mettre en ligne le site et son administration, il faut mettre en place une authentification sur react-admin.
Une mise à jour récente de React-Admin a fait apparaitre un problème au niveau de l'implémentation actuelle du tri (issue #56) et de la pagination (issue #55) des listes de l'API.
L'objet filtre utilisé actuellement n'est lui pas impacté par le problème de JSON.stringify
du sort
et du pagination
. Pour autant, l'implémentation des filtres est-elle satisfaisante ?
Pour rappel, voici comment est déclarée la pagination dans la contrat OpenAPI :
// https://github.com/CaenCamp/jobs-caen-camp/blob/master/apps/api/openapi/openapi.yaml#L1052
Filter:
name: filters
in: query
description: "Filtres appliquables à la requête. C'est un objet json stringifié, avec les filtres en clé/valeur séparé par des virgules."
required: false
explode: true
schema:
type: string
Nous avons donc un objet serialisé en json, portant les clé/valeur des filtres à appliquer.
Une de problème évidant est la difficulté à documenter proprement cet objet filtre générique, en détaillant les clés applicable en fonction des objets filtrés.
Et regardons ce que disent les Bests practices concernant les filtres d'API :
Il semble donc qu'une première bonne pratique consiste tout d'abord à extraire chaque filtre en tant que paramètre de requête à part entière.
Ce qui transformerait une ancienne requête /api/organizations?filters=%7B%22title%22%3A%22toto%22%2C%22postalCode%22%3A1400%7D
en api/organizations?title=toto&postalCode=14000
L'autre point / bonne pratique concerne la manière dont sera appliqué le filtre. En effet, pour le moment, nous avons de filtre qui ont des comportements différents et des nommages ne correspondant plus au nom de la propriété que l'on filtre. Par exemple, pour l'objet jobPosting
, nous avons des filtres :
employmentType
qui filtre sur l'integralité de la valeur passée (en sql : WHERE employment_type = 'value'
),title
qui filtre sur un LIKE de la valeur passée (en sql : WHERE title LIKE '%value'
),jobStartDate_after
qui filtre sur une date de début d'emploi supérieure à la date passée en paramètre (en sql : WHERE jobStartDate > dateValue
)Et encore une fois, tout ceci est en l'état difficilement documentable via OpenAPI.
Pour répondre à ce probléme, je propose l'adoption des RHS Colon en les adaptant légèrement, c'est à dire : key=value(:operator)
L'opérateur devient alors optionnel, avec une valeur par défault eq
(=). Ceci semble en effet plus facile à utiliser pour l'utilisateur final.
Voici une liste d'opérateur à implémenter :
operateur | applicable à | explication |
---|---|---|
:eq | string, number, date | Is equal to |
:gt | number, date | Is greater than |
:lt | number, date | Is less than |
:gte | number, date | Is greater than or equal to |
:lte | number, date | Is less than or equal to |
Ce qui transformerait une ancienne requête /api/organizations?filters={ employmentType: 'CDD', jobStartDate_after='2020/09/01' }
en api/organizations?employmentType=CDD&jobStartDate=2020/09/01:gte
Concernant les recherche de %LIKE
, je propose de regrouper toutes ces requêtes sur un paramètre unique q
qui effectura toujours une recherche de type %LIKE
en base de données, quand bien même ce LIKE
devrait être appliqué sur plusieurs champs en même temps.
Ce qui transformerait une ancienne requête /api/organizations?filters={ title: 'Javasc' }
en api/organizations?q=Javasc
ADR
concernant cette nouvelle implémentation (pour l'historique. Ne pas hésiter à reprendre la présente issue)router
.s de job-posting
et organization
repository
de job-posting
et organization
dataProvider
de react-adminBref, c'est une très bonne occasion de rentrer dans le code du projet !
Tout comme le problème de pagination décrit dans l'issue #55, une mise à jour récente de React-Admin a fait apparaitre un problème au niveau de l'implémentation actuelle du tri des listes de l'API.
Pour rappel, voici comment est déclarée la pagination de la contrat OpenAPI :
// https://github.com/CaenCamp/jobs-caen-camp/blob/master/apps/api/openapi/openapi.yaml#L1060
Sort:
name: sort
in: query
description: "Le tri applicable à la liste. C'est un tableau stringifié de la forme [sortProp, sortDirection]"
required: false
explode: false
schema:
type: array
maxItems: 2
minItems: 2
items:
type: string
Même cause, même effet, le problème vient donc de l'utilisation d'un tableau stringifié pour gérer le tri. (Encore une fois, la lecture de l'issue #55 vous renseignera sur le principe du problème à adresser dans cette issue).
Revenons à quelques Best Practices :
Je propose donc comme dans le cas de la pagination d'extraire les deux paramètres de tri utilisés dans la tableau sort
en deux paramètres : sortBy
et orderBy
Ce qui transformerait une ancienne requête /api/organizations?sort=%5B%22title%22%2C%22DESC%22%5D
en api/organizations?sortBy=title&orderBy=DESC
On pourrait débattre du camelCase
utilisé sur le nom de ces paramètres. Une url n'est pas sensible à la casse. En effet, le W3C dit:
There may be URLs, or parts of URLs, where case doesn't matter, but identifying these may not be easy. Users should always consider that URLs are case-sensitive.
Du coup, pourquoi ne pas utiliser du kebab-case
ou du snake_case
?
Et bien parce que tout les paramètres d'un objet implémenté dans notre API sont en camelCase
! Entre autre pour être compatible avec schema.org. Du coup, nous devrons transmettre les filtres par propriété (voir l'issue #XX) en camelCase
. Il semble donc préférable de rester cohérent.
Par contre, il faudra bien considérer lors du traitement de ces paramètres de requête au niveau de l'API qu'ils ne doivent pas être sensibles à la casse !
ADR
concernant cette nouvelle implémentation (pour l'historique. Ne pas hésiter à reprendre la présente issue)router
.s de job-posting
et organization
repository
de job-posting
et organization
dataProvider
de react-adminBref, c'est une très bonne occasion de rentrer dans le code du projet !
Le contenu de site est prévu pour s’afficher sur une application web. Mais ce front n’est pas nécessairement le seul consommateur de l’API. Nous pourrions mettre un place un flux RSS
Dans le Makefile
et les docker-compose.yml
, nous mappons les UID
et GUID
de l'utilisateur de l'hôte sur l'utilisateur du conteneur. Cela permet par exemple que cet utilisateur soit propriétaire de tous les fichiers créés dans le conteneur (comme les node_modules/
) sur les volumes montés entre l'hote et le containeur.
Pour faire cela, nous utilisions ceci en début de Makefile
export CURRENT_UID ?= $(shell id -u):$(shell id -g)
Mais certains environnement (mac ?, windows ?) ne supportaient pas l'utilisation du shell
. Cette commande est alors devenu :
export CURRENT_UID ?= $(id -u):$(id -g)
Mais cela pose des problème au niveau de la CI (Github actions).
Du coup, ce patch a été mis en place
CURRENT_UID=$(id -u):$(id -g)
export CURRENT_UID ?= $(shell id -u):$(shell id -g)
Si cela semble avoir réglé le provlème sur les environnement prédédement cité, maintenant, cela ne fonction plus sous Linux (Ubuntu)
Il faudrait trouver une solution fiable pour régler ce problème.
Le contenu de site est prévu pour s’afficher sur une application web. Mais ce front n’est pas nécessairement le seul consommateur de l’API. Nous pourrions mettre un place un bot pour publier les offres d’emplois sur Slack
Certain type de contenu (la description d’une entreprise, la description des offre d’emploi) ne permet pas pour le moment de faire une mise en forme du contenu. Il faudrait permettre d’appliquer un balisage markdown à ces contenus.
Histoire de faire la jonction entre le front et l'API, j'ai commencé à jeter un œil aux stores "à la Svelte" : https://svelte.dev/examples#custom-stores
Ça semble nettement plus light que Redux, mais j'imagine que ça pourrait faire le job.
Avez-vous des réserves ou des préconisations dans ce domaine ?
Le contrat OpenAPI décrit les requêtes de type json (application/json). Le format des données respectant la description des entreprises et des offres d’emploi décrite par schema.org, il pourrait être intéressant de rajouter la description des requêtes sur le type application/json-ld
Notre modèle d’offre d’emploi est encore très minimal, et se calque sur le modèle décrit dans schema.org
{
"title": "Developpeur Javascript",
"url": "https://jobs.caen.camp",
"datePosted": "2010-03-02",
"employerOverview": "Au sein d'une équipe DSI composée de juniors et de séniors",
"employmentType": "CDD",
"experienceRequirements": "3 ans d'experience sur un projet Javascript",
"jobStartDate": "2020-05-02",
"skills": "JavaScript, Devops, Php, ...",
"validThrough": "2010-05-05",
"hiringOrganization": {
"identifier": "a122edec-5580-4a93-aff7-fc18b41e4c57",
"name": "Incaya",
"image": "https://www.incaya.fr/static/logo-incaya.svg",
"url": "https://www.incaya.fr/",
"address": {
"addressCountry": "France",
"addressLocality": "Caen",
"postalCode": "14000"
}
}
}
Mais cela ne répond pas forcement à vos/nos attentes en tant que développeurs !
Que souhaiteriez-vous ajouter à ce modèle ? Quelles sont les informations qui vous intéresse vraiment lorsque vous regardez une offre d’emploi ? Et celle dont finalement on se moque ? Quelles sont les informations que vous aimeriez avoir et que vous ne trouvez jamais ?
Le contenu de site est prévu pour s’afficher sur une application web. Mais ce front n’est pas nécessairement le seul consommateur de l’API. Nous pourrions mettre un place un mécanisme de diffusion des offres d’emploi via un flux Activity Streams
Eslint, hysky pour hook git (https://github.com/typicode/husky), conf vscode, etc ...
Une mise à jour récente de React-Admin a fait apparaitre un problème au niveau de l'implémentation actuelle de la pagination des listes de l'API.
Pour rappel, voici comment est déclarée la pagination de la contrat OpenAPI :
// dans jobs-caen-camp/apps/api/openapi/openapi.yaml, ligne 1072
// https://github.com/CaenCamp/jobs-caen-camp/blob/master/apps/api/openapi/openapi.yaml#L1072
Pagination:
name: pagination
in: query
description: "Les paramètres pour la pagination. C'est un tableau stringifié de la forme [perPage, currentPage]"
required: false
explode: false
schema:
type: array
maxItems: 2
minItems: 2
items:
type: string
Soit un tableau de type [perPage, currentPage].
Alors tout d'abord, l'utilisation d'un tableau à deux valeurs n'est pas forcement très parlant. On se pose un peu toujours le question de savoir à quoi correspond le premier et le second, ces paramètres n'étant pas nommés.
Mais un second problème provient du fait que c'est un tableau qui est déclaré dans OpenAPI, et non un string.
Hors, voici ce que faisait jusqu'à présent le dataProvider
de react-admin
// https://github.com/CaenCamp/jobs-caen-camp/blob/master/apps/admin/src/jobBoardDataProvider.js#L24
const query = {
sort: JSON.stringify([field, order]),
filters: JSON.stringify(params.filter),
pagination: JSON.stringify([perPage, page])
};
C'est à dire que l'on transforme le tableau en string
. C'est d'ailleurs ce qui est implémenter dans tous les tests, par exemple :
// https://github.com/CaenCamp/jobs-caen-camp/blob/master/tests-e2e/api/job-postings.spec.js#L139
await frisby
.get(
`http://api:3001/api/job-postings?pagination=${JSON.stringify(
[2, 1]
)}`
)
...
Or depuis la mise à jours de react-admin, voici ce que retourne le serveur :
RequestValidationError: Schema validation error (/pagination: minItems should NOT have fewer than 2 items)
Le premier patch envisagé à consister à ne plus stringifier pagination depuis la dataProvider
. Cela a effectivement réglé le problème de l'erreur, mais du coup, la pagination ne fonctionnait plus. Ce qui est logique, puisque si l'on regarde ce que fait l'API :
// https://github.com/CaenCamp/jobs-caen-camp/blob/master/apps/api/src/job-posting/router.js#L20
const { jobPostings, contentRange } = await getJobPostingPaginatedList({
client: ctx.db,
filters: parseJsonQueryParameter(ctx.query.filters),
sort: parseJsonQueryParameter(ctx.query.sort),
pagination: parseJsonQueryParameter(ctx.query.pagination),
});
// https://github.com/CaenCamp/jobs-caen-camp/blob/f474868ba4d81456c7a8a97ce256cd27f3acbc51/apps/api/src/toolbox/sanitizers.js#L96
const parseJsonQueryParameter = parameter => {
if (parameter === undefined) {
return false;
}
try {
return JSON.parse(parameter);
} catch (e) {
return false;
}
};
C'est à dire que l'on s'attend au niveau de l'API à avoir une pagination stringifiée, qu'on essaye de la parser, et que l'on renvoi false
si on a une erreur. C'est ce qui se passe si l'on ne fait plus pagination: parseJsonQueryParameter(ctx.query.pagination)
dans le dataProvider
.
Du coup, le seconde solution de résolution de problème a été de modifier le contrat OpenAPI en déclarant une string
pour la pagination plutôt qu'un array
(comme pour les filters, ce qui est un autre problème, voir l'issue #). Si cela resolvait la problème côté react-admin, cela a provoqué beaucoup d'echec dans les tests déjà implémenté ...
Et c'est là qu'on peut commencer à se dire sérieusement : "cette manière de gérer la pagination est-elle vraiment la bonne ?"
En se renseignant un peu sur les Best Practices en la matière, il semblerait bien que non ^^
Voici donc la nouvelle implémentation proposée pour la pagination :
page
et perPage
Ce qui consiste simplement à renvoyer les paramètres qui étaient jusqu'à présent envoyés dans le tableau pagination
en deux paramètre explicitement nommés.
Ce qui transformerait une ancienne requête /api/organizations?pagination=%22%5B10%2C1%5D%22
en api/organizations?page=1&perPage=10
On pourrait discuter de méthode plus efficiente de pagination (Fast pagination on PostgreSQL ou Five ways to paginate in Postgres, from the basic to the exotic), mais pour autant, et vu nos besoins et nos volumes de données les plus optimistes, il semble que page
et perPage
soit très compréhensibles et ils conviennent parfaitement à l'implémentation de la pagination déjà en place.
Content-Range
comme support d'information sur la paginationJusqu'à présent, nous utilisons le header
http Content-Range
pour renvoyer les informations concernant la pagination (information sur la page en cours, le nombre de résultats retournés et le nombre total de page). Cela nous permet entre autre de ne pas utiliser un formatage du retour d'API qui inclurait les informations de pagination (problématique de HATEOAS)
// https://github.com/CaenCamp/jobs-caen-camp/blob/f474868ba4d81456c7a8a97ce256cd27f3acbc51/apps/api/src/toolbox/sanitizers.js#L85
/**
* Transforms the Knex paging object into a string compatible with the "content-Range" header.
* https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/Content-Range
*
* @param {string} objectType - type of object returned in the paginated collection
* @param {object} pagination - Knex pagination object (https://github.com/felixmosh/knex-paginate#pagination-object)
* @returns {string} string ready to be set has "Content-Range" http header
* @example Content-Range: posts 0-24/319
*/
const formatPaginationContentRange = (objectType, pagination) =>
`${objectType.toLowerCase()} ${pagination.from}-${pagination.to}/${
pagination.total
}`;
Or, si on lit bien la spécification de Content-Range on se rend vite compte que l'utilisation de ce header pour la pagination n'est pas la bonne manière de faire, car ce header
a une autre signification.
The Content-Range response HTTP header indicates where in a full body message a partial message belongs.
Ce qui correspond donc à un retour http de type 206 Partial Content
.
header
http X-Total-Count
Donc, pour ne pas avoir à reformater le corps de notre réponse pour y inclure la pagination, mais retourner un paramètre indispensable à la pagination, c'est à dire le nombre total de résultats possible, nous allons implémenter une header http spécifique : le X-Total-Count
. C'est un header certes spécifique, mais assez largement utilisé.
Link
dans le header
concernant la paginationToujours pour ne pas à avoir à implémenter une structure particulière au body
de la réponse de l'API, nous allons rajouter une ou des balises Link
dans l'en-tête de la réponse afin de respecter les bonnes pratiques. L'utilisation de cette balise est décrite dans ce post Link header introduced by RFC 5988 et prendrait la forme :
Link: <https://job.caen.camp/api/organization?page=2&perPage=10>; rel="next", <https://job.caen.camp/api/organization?page=6&perPage=10>; rel="last"
On pourra s'inspirer de l'implémentation de Github.
ADR
concernant cette nouvelle implémentation (pour l'historique. Ne pas hésiter à reprendre la présente issue)router
.s de job-posting
et organization
repository
de job-posting
et organization
dataProvider
de react-adminBref, c'est une très bonne occasion de rentrer dans le code du projet !
le make install ne lance pas l’installation des dépendance javascript de l’administration (qui est en dehore du workspace Yarn)
Je rencontre un soucis de droits d'écriture lors du make install
, et plus particulièrement au moment d'écrire dans le cache de Cypress (install Docker).
Si j'en crois cypress-io/cypress#1281 on aurait peut-être intérêt à préciser CYPRESS_CACHE_FOLDER (env).
error /jobboard/node_modules/cypress: Command failed.
Exit code: 1
Command: node index.js --exec install
Arguments:
Directory: /jobboard/node_modules/cypress
Output:
Cypress cannot write to the cache directory due to file permissions
See discussion and possible solutions at
https://github.com/cypress-io/cypress/issues/1281
----------
Failed to access /.cache/Cypress:
EACCES: permission denied, mkdir '/.cache'
(MacOS 10.14.6)
Avis bienvenus !
Bonjour à tous,
j'ai une petite question concernant l'un des choix pour la partie frontend: le router de svelte.
pourquoi etre parti sur svelte-routing
et non sur la solution officiel Sapper ?
Mise en place de l'intégration continue via Github Actions
Après avoir refondu la gestion de la pagination, du tri et des filtres, il serait chouette de mettre à jour le front (en tout cas React-admin) pour qu'il ne tente plus d'appeler le back ainsi :
job-postings?filter=%7B%22jobStartDate_before%22%3A%222020-05-05%22%7D&order=ASC&page=1&perPage=10&sort=name
mais ainsi :
job-postings?jobStartDate:lt=2020-05-05&sortBy=title&orderBy=ASC¤tPage=1&perPage=10
Le contrat OpenApi contient les specs nécessaires, d'ailleurs consultables avec Swagger sur l'endpoint /documentation
de l'API.
Je vais me pencher dessus, même si je ne connais pas encore Svelte ou react.
Je pense que ça doit être une bonne porte d'entrée dans le code du front, aussi je mets le label good first issue
.
Le make file declare 2 variables d'environnement au début du fichier:
export UID = $(shell id -u)
export GID = $(shell id -g)
Si la commande shell n'existe pas, aucune erreur n'est remonté mais le yarn install
va échouer car il n'aura pas les bonnes permissions étant donné que le UID & le GID ne seront pas valides ...
Le fichier openapi est en yaml, avec des tabulations larges de deux espaces. VScode me propose deux linters différents, mais comment les configurer ? J'ai peur de tout casser.
Ce qui serait super, ce serait d'avoir un fichier du genre .ymllinterrc
avec les règles de formatages pour le yaml, dans le fichier racine. Qu'en pensez-vous ? Connaissez-vous des solutions ?
Le format des données respectant la description des entreprises et des offres d’emploi décrite par schema.org, il pourrait être intéressant de servir des réponses au format json-ld
Proposition :
Dans la documentation Swagger génére depuis le contrat openAPI, il n’y a pas d’exemple pour les paramètres de requêtes pour les listes paginées (filters, sort et pagination).
Ce serait un plus que de rajouter ces exemples au contrat
Pour le moment, le Makefile
du projet s'appuie sur un environnement de developpement local utilisant Docker.
Mais c'est un parti pris contestable et nous ne devrions pas imposer l'utilisation de Docker et Docker Compose.
Il faudrait donc trouver un moyen de rendre le Makefile
utilisable, en utilisant Docker, ou pas !
Je n'ai pas trop d'idées pour le moment sur le comment faire ... Ajout d'une variable d'environnement à la charge du développeur ? Detection du l'existance de Docker Compose en début de Makefile ?
A voir.
Toujours est-il que ce fix devrait être documenter dans le guide de contribution.
Et idéallement être expliqué/documenté hors du context du jobBoard dans le coding-caen-camp afin d'en faire profiter d'autre projets
Comment allons nous structurer les données ?
Lors de cette mise en place, nous avons convenu de créer le modèle sous forme d'objets json afin de les implémenter rapidement au sein du service "api-mocked".
Avec la mise en place de react-admin, il s'impose la nécessité d'utiliser les workspaces yarn pour mieux gérer les différents "packages".
CF cette PR : #36
J'ai réussi à mettre en place, sur un autre projet, les workspaces avec plusieurs CRA en utilisant craco, un petit tool qui permet d'override la config webpack pour passer les composants reacts des packages sur babel-loader :
https://github.com/gsoft-inc/craco
le fichier craco.config.js en question :
const path = require('path');
const { getLoader, loaderByName } = require('@craco/craco');
// Transform into an array if you want to include more babelized packages
const absolutePath = path.join(__dirname, 'packages/core/src');
module.exports = {
webpack: {
alias: {},
plugins: [],
configure: webpackConfig => {
const { isFound, match } = getLoader(webpackConfig, loaderByName('babel-loader'));
if (isFound) {
const include = Array.isArray(match.loader.include)
? match.loader.include
: [match.loader.include];
include.push(absolutePath); // Add core packages to the babel-loader list. Use concat in case of array.
match.loader.include = include; // Push it to the config
}
return webpackConfig;
},
},
};
Cette snippet permet dans un package, de consommer des composants d'un autre package en les passant via babel-loader.
ll suffit ensuite d'utiliser les commandes craco à la place de react-scripts dans le package.json en question.
Que pensez-vous de ce système ?
Je l'ai mis en place dans ma boîte, ça fait 3 semaines que ça tourne bien.
Nous utilisions avant https://github.com/react-workspaces/create-react-app mais il ne semble plus activement maintenu et c'est un remplaçant à CRA tandis que Craco n'est qu'une couche d'override.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.