@20syldev/api 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/changelog.md +333 -0
- package/package.json +5 -4
- package/src/app.ts +7 -3
- package/src/config/versions.ts +22 -10
- package/src/constants.ts +19 -0
- package/src/middleware/cors.ts +1 -1
- package/src/modules/v4/algorithms.ts +43 -1
- package/src/modules/v4/chat.ts +24 -1
- package/src/modules/v4/dice.ts +39 -0
- package/src/modules/v4/encode.ts +170 -0
- package/src/modules/v4/geo.ts +53 -0
- package/src/modules/v4/palette.ts +103 -0
- package/src/modules/v4/placeholder.ts +122 -0
- package/src/modules/v4/statistics.ts +55 -0
- package/src/modules/v4/text.ts +300 -0
- package/src/modules/v4/tic_tac_toe.ts +33 -1
- package/src/modules/v4/validate.ts +55 -0
- package/src/modules/v4.ts +2 -0
- package/src/routes/delete.ts +72 -0
- package/src/routes/get.ts +47 -0
- package/src/routes/index.ts +2 -2
- package/src/routes/patch.ts +45 -0
- package/src/utils/version.ts +5 -0
- package/tests/integration/api.test.ts +395 -0
- package/tests/tsconfig.json +3 -0
- package/tests/unit/algorithms.test.ts +120 -0
- package/tests/unit/color.test.ts +33 -0
- package/tests/unit/convert.test.ts +42 -0
- package/tests/unit/dice.test.ts +57 -0
- package/tests/unit/domain.test.ts +47 -0
- package/tests/unit/encode.test.ts +108 -0
- package/tests/unit/geo.test.ts +45 -0
- package/tests/unit/hash.test.ts +37 -0
- package/tests/unit/hyperplanning.test.ts +77 -0
- package/tests/unit/levenshtein.test.ts +34 -0
- package/tests/unit/palette.test.ts +53 -0
- package/tests/unit/personal.test.ts +65 -0
- package/tests/unit/statistics.test.ts +57 -0
- package/tests/unit/text.test.ts +107 -0
- package/tests/unit/time.test.ts +67 -0
- package/tests/unit/token.test.ts +61 -0
- package/tests/unit/username.test.ts +34 -0
- package/tests/unit/validate.test.ts +71 -0
- package/tsconfig.test.json +9 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<a href="https://api.sylvain.sh"><img src="https://api.sylvain.sh/favicon.ico" alt="Logo" width="25%" height="auto"/></a>
|
|
3
3
|
|
|
4
4
|
# API Personnelle
|
|
5
|
-
[](https://github.com/20syldev/api/releases/latest)
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -32,10 +32,10 @@ Pour démarrer un serveur local avec tous les endpoints :
|
|
|
32
32
|
$ npm run build && npm start
|
|
33
33
|
```
|
|
34
34
|
```console
|
|
35
|
-
> @20syldev/api@4.
|
|
35
|
+
> @20syldev/api@4.1.0 build
|
|
36
36
|
> tsc
|
|
37
37
|
|
|
38
|
-
> @20syldev/api@4.
|
|
38
|
+
> @20syldev/api@4.1.0 start
|
|
39
39
|
> node dist/app.js
|
|
40
40
|
|
|
41
41
|
API is running on
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Exemples curl
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# dice
|
|
7
|
+
curl -X GET "http://127.0.0.1:3000/v4/dice?roll=2d6%2B3"
|
|
8
|
+
|
|
9
|
+
# encode
|
|
10
|
+
curl -X GET "http://127.0.0.1:3000/v4/encode?method=base64encode&text=hello"
|
|
11
|
+
curl -X GET "http://127.0.0.1:3000/v4/encode?method=morse&text=SOS"
|
|
12
|
+
curl -X GET "http://127.0.0.1:3000/v4/encode?method=caesar&text=hello&shift=3"
|
|
13
|
+
|
|
14
|
+
# geo
|
|
15
|
+
curl -X GET "http://127.0.0.1:3000/v4/geo?lat1=48.8566&lon1=2.3522&lat2=43.2965&lon2=5.3698"
|
|
16
|
+
|
|
17
|
+
# palette
|
|
18
|
+
curl -X GET "http://127.0.0.1:3000/v4/palette?color=%23ff6600&type=triadic"
|
|
19
|
+
|
|
20
|
+
# statistics
|
|
21
|
+
curl -X GET "http://127.0.0.1:3000/v4/statistics?values=1,2,3,4,5"
|
|
22
|
+
|
|
23
|
+
# text - stats
|
|
24
|
+
curl -X GET "http://127.0.0.1:3000/v4/text?method=stats&value=Hello+world"
|
|
25
|
+
|
|
26
|
+
# text - slug
|
|
27
|
+
curl -X GET "http://127.0.0.1:3000/v4/text?method=slug&value=Héllo+Wörld%21"
|
|
28
|
+
|
|
29
|
+
# text - lorem
|
|
30
|
+
curl -X GET "http://127.0.0.1:3000/v4/text?method=lorem&type=sentences&count=3"
|
|
31
|
+
|
|
32
|
+
# text - number
|
|
33
|
+
curl -X GET "http://127.0.0.1:3000/v4/text?method=number&value=42&lang=fr"
|
|
34
|
+
|
|
35
|
+
# validate - luhn
|
|
36
|
+
curl -X GET "http://127.0.0.1:3000/v4/validate?type=luhn&value=4532015112830366"
|
|
37
|
+
|
|
38
|
+
# validate - iban
|
|
39
|
+
curl -X GET "http://127.0.0.1:3000/v4/validate?type=iban&value=FR7630006000011234567890189"
|
|
40
|
+
|
|
41
|
+
# validate - email
|
|
42
|
+
curl -X GET "http://127.0.0.1:3000/v4/validate?type=email&value=user%40example.com"
|
|
43
|
+
|
|
44
|
+
# algorithms - roman
|
|
45
|
+
curl -X GET "http://127.0.0.1:3000/v4/algorithms?method=roman&value=42"
|
|
46
|
+
curl -X GET "http://127.0.0.1:3000/v4/algorithms?method=roman&value=XLII"
|
|
47
|
+
|
|
48
|
+
# REST v4.2 - jouer un coup (PATCH)
|
|
49
|
+
curl -X PATCH "http://127.0.0.1:3000/v4/tic-tac-toe/{gameId}" \
|
|
50
|
+
-H "Content-Type: application/json" \
|
|
51
|
+
-d '{"username":"alice","move":"A1","session":"abc123"}'
|
|
52
|
+
|
|
53
|
+
# REST v4.2 - forfait (DELETE tic-tac-toe)
|
|
54
|
+
curl -X DELETE "http://127.0.0.1:3000/v4/tic-tac-toe/{gameId}" \
|
|
55
|
+
-H "Content-Type: application/json" \
|
|
56
|
+
-d '{"username":"alice","session":"abc123"}'
|
|
57
|
+
|
|
58
|
+
# REST v4.2 - vider un chat (DELETE chat)
|
|
59
|
+
curl -X DELETE "http://127.0.0.1:3000/v4/chat/{token}" \
|
|
60
|
+
-H "Content-Type: application/json" \
|
|
61
|
+
-d '{"username":"alice","session":"abc123"}'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## v4.2.0
|
|
67
|
+
|
|
68
|
+
### REST endpoints pour tic-tac-toe et chat
|
|
69
|
+
|
|
70
|
+
Les endpoints existants ont été étendus avec des méthodes HTTP REST. Uniquement disponible en `v4+`.
|
|
71
|
+
|
|
72
|
+
#### PATCH `/v4/tic-tac-toe/:game`
|
|
73
|
+
|
|
74
|
+
Jouer un coup dans une partie.
|
|
75
|
+
|
|
76
|
+
**Body (JSON) :**
|
|
77
|
+
| Param | Type | Description |
|
|
78
|
+
|-------|------|-------------|
|
|
79
|
+
| `username` | string | Nom du joueur |
|
|
80
|
+
| `move` | string | Case jouée (ex: `A1`) |
|
|
81
|
+
| `session` | string | ID de session |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
#### DELETE `/v4/tic-tac-toe/:game`
|
|
86
|
+
|
|
87
|
+
Abandonner une partie (forfeit).
|
|
88
|
+
|
|
89
|
+
**Body (JSON) :**
|
|
90
|
+
| Param | Type | Description |
|
|
91
|
+
|-------|------|-------------|
|
|
92
|
+
| `username` | string | Nom du joueur |
|
|
93
|
+
| `session` | string | ID de session |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
#### DELETE `/v4/chat/:token`
|
|
98
|
+
|
|
99
|
+
Effacer tous les messages d'un chat privé (clear).
|
|
100
|
+
|
|
101
|
+
**Body (JSON) :**
|
|
102
|
+
| Param | Type | Description |
|
|
103
|
+
|-------|------|-------------|
|
|
104
|
+
| `username` | string | Nom de l'utilisateur |
|
|
105
|
+
| `session` | string | ID de session |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### Autres changements v4.2.0
|
|
110
|
+
|
|
111
|
+
- CORS : ajout de `PATCH`, `DELETE`, `OPTIONS` dans les headers autorisés
|
|
112
|
+
- Nouveau helper `supportsRest()` dans `src/utils/version.ts`
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## v4.1.0
|
|
117
|
+
|
|
118
|
+
### Nouveaux endpoints
|
|
119
|
+
|
|
120
|
+
#### GET `/v4/dice`
|
|
121
|
+
|
|
122
|
+
Lance des dés en notation RPG.
|
|
123
|
+
|
|
124
|
+
**Query params :**
|
|
125
|
+
| Param | Requis | Description | Exemple |
|
|
126
|
+
|-------|--------|-------------|---------|
|
|
127
|
+
| `roll` | oui | Notation NdX ou NdX+M | `2d6+3` |
|
|
128
|
+
|
|
129
|
+
**Note :** le `+` dans l'URL peut être encodé en espace (`2d6 3`) — les deux sont acceptés.
|
|
130
|
+
|
|
131
|
+
**Réponse :**
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"roll": "2d6+3",
|
|
135
|
+
"count": 2,
|
|
136
|
+
"sides": 6,
|
|
137
|
+
"modifier": 3,
|
|
138
|
+
"results": [4, 2],
|
|
139
|
+
"total": 9
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Limites : 1–100 dés, 2–1000 faces.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
#### GET `/v4/encode`
|
|
148
|
+
|
|
149
|
+
Encode ou décode du texte dans différents formats.
|
|
150
|
+
|
|
151
|
+
**Query params :**
|
|
152
|
+
| Param | Requis | Description |
|
|
153
|
+
|-------|--------|-------------|
|
|
154
|
+
| `method` | oui | Voir liste ci-dessous |
|
|
155
|
+
| `value` | oui | Texte à traiter |
|
|
156
|
+
| `shift` | non | Décalage pour Caesar (défaut : 13) |
|
|
157
|
+
|
|
158
|
+
**Méthodes disponibles :**
|
|
159
|
+
| `method` | Description |
|
|
160
|
+
|----------|-------------|
|
|
161
|
+
| `base64encode` | Encode en Base64 |
|
|
162
|
+
| `base64decode` | Décode du Base64 |
|
|
163
|
+
| `urlencode` | Encode pour URL |
|
|
164
|
+
| `urldecode` | Décode une URL |
|
|
165
|
+
| `morse` | Texte → Morse |
|
|
166
|
+
| `unmorse` | Morse → Texte |
|
|
167
|
+
| `rot13` | ROT-13 |
|
|
168
|
+
| `caesar` | Chiffre de César (nécessite `shift`) |
|
|
169
|
+
| `binary` | Texte → binaire 8 bits |
|
|
170
|
+
| `unbinary` | Binaire → texte |
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
#### GET `/v4/geo`
|
|
175
|
+
|
|
176
|
+
Calcule la distance et le cap entre deux coordonnées GPS (Haversine).
|
|
177
|
+
|
|
178
|
+
**Query params :**
|
|
179
|
+
| Param | Requis | Description |
|
|
180
|
+
|-------|--------|-------------|
|
|
181
|
+
| `lat1` | oui | Latitude point A (-90/+90) |
|
|
182
|
+
| `lon1` | oui | Longitude point A (-180/+180) |
|
|
183
|
+
| `lat2` | oui | Latitude point B |
|
|
184
|
+
| `lon2` | oui | Longitude point B |
|
|
185
|
+
|
|
186
|
+
**Réponse :**
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"distance": { "km": 1214.652, "miles": 754.801, "nauticalMiles": 655.879 },
|
|
190
|
+
"bearing": { "degrees": 157.34, "cardinal": "SSE" },
|
|
191
|
+
"from": { "lat": 48.8566, "lon": 2.3522 },
|
|
192
|
+
"to": { "lat": 43.2965, "lon": 5.3698 }
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
#### GET `/v4/palette`
|
|
199
|
+
|
|
200
|
+
Génère une palette de couleurs harmonieuse à partir d'une couleur HEX.
|
|
201
|
+
|
|
202
|
+
**Query params :**
|
|
203
|
+
| Param | Requis | Description |
|
|
204
|
+
|-------|--------|-------------|
|
|
205
|
+
| `color` | oui | Couleur base en HEX (`#RRGGBB`) |
|
|
206
|
+
| `type` | oui | Type de palette |
|
|
207
|
+
|
|
208
|
+
**Types disponibles :**
|
|
209
|
+
| `type` | Couleurs générées |
|
|
210
|
+
|--------|------------------|
|
|
211
|
+
| `complementary` | 2 couleurs (opposées) |
|
|
212
|
+
| `triadic` | 3 couleurs (120°) |
|
|
213
|
+
| `analogous` | 5 couleurs (-60° à +60°) |
|
|
214
|
+
| `tetradic` | 4 couleurs (90°) |
|
|
215
|
+
| `split-complementary` | 3 couleurs |
|
|
216
|
+
|
|
217
|
+
Chaque couleur retournée a `hex`, `rgb`, `hsl`.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
#### GET `/v4/statistics`
|
|
222
|
+
|
|
223
|
+
Calcule des statistiques descriptives sur une liste de nombres.
|
|
224
|
+
|
|
225
|
+
**Query params :**
|
|
226
|
+
| Param | Requis | Description |
|
|
227
|
+
|-------|--------|-------------|
|
|
228
|
+
| `values` | oui | Nombres séparés par des virgules |
|
|
229
|
+
|
|
230
|
+
**Réponse :**
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"count": 5,
|
|
234
|
+
"sum": 15,
|
|
235
|
+
"min": 1,
|
|
236
|
+
"max": 5,
|
|
237
|
+
"range": 4,
|
|
238
|
+
"mean": 3,
|
|
239
|
+
"median": 3,
|
|
240
|
+
"mode": [],
|
|
241
|
+
"variance": 2,
|
|
242
|
+
"stddev": 1.414214
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
#### GET `/v4/text`
|
|
249
|
+
|
|
250
|
+
Manipulation et génération de texte.
|
|
251
|
+
|
|
252
|
+
**Query params :**
|
|
253
|
+
| Param | Requis | Description |
|
|
254
|
+
|-------|--------|-------------|
|
|
255
|
+
| `action` | oui | Voir liste ci-dessous |
|
|
256
|
+
| `value` | non | Texte source (selon action) |
|
|
257
|
+
| `type` | non | `words`, `sentences`, `paragraphs` (pour `lorem`) |
|
|
258
|
+
| `count` | non | Nombre d'éléments (pour `lorem`, défaut 5) |
|
|
259
|
+
| `lang` | non | `fr` ou `en` (pour `number`) |
|
|
260
|
+
|
|
261
|
+
**Actions disponibles :**
|
|
262
|
+
| `action` | Description | Params requis |
|
|
263
|
+
|----------|-------------|---------------|
|
|
264
|
+
| `stats` | Statistiques du texte | `value` |
|
|
265
|
+
| `slug` | Convertit en slug URL | `value` |
|
|
266
|
+
| `lorem` | Génère du Lorem Ipsum | `type`, `count` |
|
|
267
|
+
| `number` | Nombre en lettres | `value`, `lang` |
|
|
268
|
+
|
|
269
|
+
**Réponse `stats` :**
|
|
270
|
+
```json
|
|
271
|
+
{
|
|
272
|
+
"characters": 42,
|
|
273
|
+
"charactersNoSpaces": 35,
|
|
274
|
+
"words": 8,
|
|
275
|
+
"sentences": 2,
|
|
276
|
+
"paragraphs": 1,
|
|
277
|
+
"readingTime": "3s",
|
|
278
|
+
"mostFrequentChar": "e"
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
#### GET `/v4/validate`
|
|
285
|
+
|
|
286
|
+
Valide différents formats de données.
|
|
287
|
+
|
|
288
|
+
**Query params :**
|
|
289
|
+
| Param | Requis | Description |
|
|
290
|
+
|-------|--------|-------------|
|
|
291
|
+
| `type` | oui | `luhn`, `iban`, `email` |
|
|
292
|
+
| `value` | oui | Valeur à valider |
|
|
293
|
+
|
|
294
|
+
**Réponse `luhn` (numéro de carte) :**
|
|
295
|
+
```json
|
|
296
|
+
{ "valid": true, "value": "4532015112830366" }
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Réponse `iban` :**
|
|
300
|
+
```json
|
|
301
|
+
{ "valid": true, "value": "FR7630006000011234567890189", "country": "FR" }
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Réponse `email` :**
|
|
305
|
+
```json
|
|
306
|
+
{ "valid": true, "value": "user@example.com" }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
### Ajout à un endpoint existant
|
|
312
|
+
|
|
313
|
+
#### GET `/v4/algorithms` — conversion de chiffres romains
|
|
314
|
+
|
|
315
|
+
Nouvelle action `roman` sur l'endpoint algorithms.
|
|
316
|
+
|
|
317
|
+
**Query params :**
|
|
318
|
+
| Param | Requis | Description |
|
|
319
|
+
|-------|--------|-------------|
|
|
320
|
+
| `method` | oui | `roman` |
|
|
321
|
+
| `value` | oui | Nombre entier (1–3999) ou chiffre romain |
|
|
322
|
+
|
|
323
|
+
Bidirectionnel : `42` → `XLII`, `XLII` → `42`.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### Infrastructure v4.1.0
|
|
328
|
+
|
|
329
|
+
- **`src/app.ts`** : `export default app` + guard `NODE_ENV !== 'test'` pour les tests d'intégration
|
|
330
|
+
- **`src/config/versions.ts`** : helper `merge()` pour dédoublonner les endpoints par `name`
|
|
331
|
+
- **`tsconfig.test.json`** : étend le tsconfig principal, inclut `src/` et `tests/`
|
|
332
|
+
- **`tests/tsconfig.json`** : pointe vers `tsconfig.test.json` pour la résolution IDE
|
|
333
|
+
- **`package.json`** : script `npm test` avec `--test-force-exit` (requis car `setTimeout` dans chat/tic-tac-toe)
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.
|
|
2
|
+
"version": "4.1.0",
|
|
3
3
|
"name": "@20syldev/api",
|
|
4
4
|
"description": "Node.js API with multiple features. Check the documentation at https://docs.sylvain.sh",
|
|
5
5
|
"main": "dist/app.js",
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
"dev": "tsx watch src/app.ts",
|
|
9
9
|
"start": "node dist/app.js",
|
|
10
10
|
"build": "tsc",
|
|
11
|
-
"check": "tsc --noEmit && eslint src/",
|
|
12
|
-
"format": "prettier --write src/ && eslint --fix src/",
|
|
11
|
+
"check": "tsc --noEmit -p tsconfig.test.json && eslint src/ tests/",
|
|
12
|
+
"format": "prettier --write src/ tests/ && eslint --fix src/ tests/",
|
|
13
|
+
"test": "NODE_ENV=test node --import tsx --test --test-force-exit tests/unit/*.test.ts tests/integration/*.test.ts",
|
|
13
14
|
"upgrade:minor": "npm upgrade",
|
|
14
15
|
"upgrade:major": "npx npm-check-updates -u && npm install"
|
|
15
16
|
},
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"@eslint/js": "^9.24.0",
|
|
38
39
|
"@types/cors": "^2.8.17",
|
|
39
40
|
"@types/express": "^5.0.0",
|
|
40
|
-
"@types/node": "^22.
|
|
41
|
+
"@types/node": "^22.19.17",
|
|
41
42
|
"@types/qrcode": "^1.5.6",
|
|
42
43
|
"@types/uuid": "^10.0.0",
|
|
43
44
|
"eslint": "^9.24.0",
|
package/src/app.ts
CHANGED
|
@@ -39,6 +39,10 @@ app.use(getRoutes);
|
|
|
39
39
|
app.use(postRoutes);
|
|
40
40
|
|
|
41
41
|
// Start server
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
42
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
43
|
+
app.listen(env.PORT, () =>
|
|
44
|
+
console.log(`API is running on\n - http://127.0.0.1:${env.PORT}\n - http://localhost:${env.PORT}`),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default app;
|
package/src/config/versions.ts
CHANGED
|
@@ -11,10 +11,22 @@ export interface VersionConfig {
|
|
|
11
11
|
endpoints: {
|
|
12
12
|
get: Endpoint[];
|
|
13
13
|
post: Endpoint[];
|
|
14
|
+
patch?: Endpoint[];
|
|
15
|
+
delete?: Endpoint[];
|
|
14
16
|
};
|
|
15
17
|
modules: typeof apiv3 | typeof apiv4;
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Merges a base list of endpoints with additions, deduplicating by `name`.
|
|
22
|
+
* If an addition has the same name as a base entry, it overrides it.
|
|
23
|
+
*/
|
|
24
|
+
const merge = (base: Endpoint[], additions: Endpoint[]): Endpoint[] => {
|
|
25
|
+
const map = new Map(base.map((e) => [e.name, e]));
|
|
26
|
+
for (const e of additions) map.set(e.name, e);
|
|
27
|
+
return [...map.values()];
|
|
28
|
+
};
|
|
29
|
+
|
|
18
30
|
const v1 = {
|
|
19
31
|
get: [
|
|
20
32
|
{ name: 'algorithms', path: '/algorithms?method={algorithm}&value={value}(&value2={value2})' },
|
|
@@ -32,9 +44,8 @@ const v1 = {
|
|
|
32
44
|
};
|
|
33
45
|
|
|
34
46
|
const v2 = {
|
|
35
|
-
get:
|
|
36
|
-
post: [
|
|
37
|
-
...v1.post,
|
|
47
|
+
get: merge(v1.get, [{ name: 'chat', path: '/chat' }]),
|
|
48
|
+
post: merge(v1.post, [
|
|
38
49
|
{
|
|
39
50
|
name: 'chat',
|
|
40
51
|
children: {
|
|
@@ -51,24 +62,25 @@ const v2 = {
|
|
|
51
62
|
list: '/tic-tac-toe/list',
|
|
52
63
|
} as Record<string, string>,
|
|
53
64
|
},
|
|
54
|
-
|
|
55
|
-
],
|
|
65
|
+
]),
|
|
56
66
|
};
|
|
57
67
|
|
|
58
68
|
const v3 = {
|
|
59
|
-
get: [
|
|
60
|
-
...v2.get,
|
|
69
|
+
get: merge(v2.get, [
|
|
61
70
|
{ name: 'levenshtein', path: '/levenshtein?str1={string}&str2={string}' },
|
|
62
71
|
{
|
|
63
72
|
name: 'time',
|
|
64
73
|
path: '/time(?type={type}&start={timestamp}&end={timestamp}&format={format}&timezone={timezone})',
|
|
65
74
|
},
|
|
66
|
-
],
|
|
67
|
-
post:
|
|
75
|
+
]),
|
|
76
|
+
post: merge(v2.post, [{ name: 'hyperplanning', path: '/hyperplanning' }]),
|
|
68
77
|
};
|
|
69
78
|
|
|
70
79
|
const v4 = {
|
|
71
|
-
get:
|
|
80
|
+
get: merge(v3.get, [
|
|
81
|
+
{ name: 'dice', path: '/dice?roll={NdX+M}' },
|
|
82
|
+
{ name: 'statistics', path: '/statistics?values={n1,n2,n3,...}' },
|
|
83
|
+
]),
|
|
72
84
|
post: [...v3.post],
|
|
73
85
|
};
|
|
74
86
|
|
package/src/constants.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
2
|
+
|
|
3
|
+
export const APP_VERSION = pkg.version;
|
|
1
4
|
export const MAX_LOG_ENTRIES = 1000;
|
|
2
5
|
export const RATE_LIMIT_WINDOW = 10_000;
|
|
3
6
|
export const RATE_LIMIT_MAX = 50;
|
|
@@ -26,3 +29,19 @@ export const STATUS_MESSAGES: Record<number, string> = {
|
|
|
26
29
|
};
|
|
27
30
|
|
|
28
31
|
export const START_TIME = Date.now();
|
|
32
|
+
|
|
33
|
+
export const ROMAN_VALUES: [number, string][] = [
|
|
34
|
+
[1000, 'M'],
|
|
35
|
+
[900, 'CM'],
|
|
36
|
+
[500, 'D'],
|
|
37
|
+
[400, 'CD'],
|
|
38
|
+
[100, 'C'],
|
|
39
|
+
[90, 'XC'],
|
|
40
|
+
[50, 'L'],
|
|
41
|
+
[40, 'XL'],
|
|
42
|
+
[10, 'X'],
|
|
43
|
+
[9, 'IX'],
|
|
44
|
+
[5, 'V'],
|
|
45
|
+
[4, 'IV'],
|
|
46
|
+
[1, 'I'],
|
|
47
|
+
];
|
package/src/middleware/cors.ts
CHANGED
|
@@ -8,7 +8,7 @@ const __dirname = dirname(__filename);
|
|
|
8
8
|
|
|
9
9
|
export function setupCors(app: Express): void {
|
|
10
10
|
app.set('trust proxy', 1);
|
|
11
|
-
app.use(cors({ methods: ['GET', 'POST'] }));
|
|
11
|
+
app.use(cors({ methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'] }));
|
|
12
12
|
app.use(express.urlencoded({ extended: true }));
|
|
13
13
|
app.use(express.json());
|
|
14
14
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MAX_FACTORIAL, MAX_GCD_VALUE, MAX_PRIME_LIST, MAX_STRING_LENGTH } from '../../constants.js';
|
|
1
|
+
import { MAX_FACTORIAL, MAX_GCD_VALUE, MAX_PRIME_LIST, MAX_STRING_LENGTH, ROMAN_VALUES } from '../../constants.js';
|
|
2
2
|
|
|
3
3
|
export function anagram(value: string, value2: string): boolean {
|
|
4
4
|
if (!value) throw new Error('First value is required');
|
|
@@ -153,3 +153,45 @@ export function reverse(value: string): string {
|
|
|
153
153
|
|
|
154
154
|
return value.split('').reverse().join('');
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
export function roman(value: string): number | string {
|
|
158
|
+
if (!value) throw new Error('A value is required');
|
|
159
|
+
|
|
160
|
+
const num = Number(value);
|
|
161
|
+
if (!isNaN(num)) {
|
|
162
|
+
if (!Number.isInteger(num)) throw new Error('Value must be an integer');
|
|
163
|
+
if (num < 1 || num > 3999) throw new Error('Number must be between 1 and 3999');
|
|
164
|
+
|
|
165
|
+
let result = '';
|
|
166
|
+
let n = num;
|
|
167
|
+
for (const [val, sym] of ROMAN_VALUES) {
|
|
168
|
+
while (n >= val) {
|
|
169
|
+
result += sym;
|
|
170
|
+
n -= val;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (typeof value !== 'string') throw new Error('Value must be a number or a Roman numeral');
|
|
177
|
+
const upper = value.toUpperCase();
|
|
178
|
+
if (!/^[MDCLXVI]+$/.test(upper)) throw new Error('Invalid Roman numeral');
|
|
179
|
+
|
|
180
|
+
let result = 0;
|
|
181
|
+
let i = 0;
|
|
182
|
+
while (i < upper.length) {
|
|
183
|
+
const two = upper.slice(i, i + 2);
|
|
184
|
+
const match = ROMAN_VALUES.find(([, sym]) => sym === two);
|
|
185
|
+
if (match) {
|
|
186
|
+
result += match[0];
|
|
187
|
+
i += 2;
|
|
188
|
+
} else {
|
|
189
|
+
const one = ROMAN_VALUES.find(([, sym]) => sym === upper[i]);
|
|
190
|
+
if (!one) throw new Error('Invalid Roman numeral');
|
|
191
|
+
result += one[0];
|
|
192
|
+
i += 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result;
|
|
197
|
+
}
|
package/src/modules/v4/chat.ts
CHANGED
|
@@ -34,8 +34,10 @@ export default function chat(action: string, params: ChatParams): ChatMessage[]
|
|
|
34
34
|
return getPrivateChat(params, privateChats);
|
|
35
35
|
} else if (action === 'fetch') {
|
|
36
36
|
return fetchMessages(messages);
|
|
37
|
+
} else if (action === 'clear') {
|
|
38
|
+
return clearPrivateChat(params, privateChats, sessions, u);
|
|
37
39
|
} else {
|
|
38
|
-
throw new Error('Invalid action. Use "message", "private", or "
|
|
40
|
+
throw new Error('Invalid action. Use "message", "private", "fetch", or "clear"');
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -97,3 +99,24 @@ function fetchMessages(messages: ChatMessage[]): ChatMessage[] {
|
|
|
97
99
|
if (messages.length > 0) return messages;
|
|
98
100
|
throw new Error('No messages stored.');
|
|
99
101
|
}
|
|
102
|
+
|
|
103
|
+
function clearPrivateChat(
|
|
104
|
+
params: ChatParams,
|
|
105
|
+
privateChats: Record<string, ChatMessage[]>,
|
|
106
|
+
sessions: Record<string, { user: string; last: number }>,
|
|
107
|
+
u: string,
|
|
108
|
+
): { message: string } {
|
|
109
|
+
const { token, session } = params;
|
|
110
|
+
|
|
111
|
+
if (!token) throw new Error('Please provide a valid token');
|
|
112
|
+
if (!session) throw new Error('Please provide a valid session ID');
|
|
113
|
+
if (sessions[u] && sessions[u].user !== session) {
|
|
114
|
+
throw new Error('Session ID mismatch');
|
|
115
|
+
}
|
|
116
|
+
if (!privateChats[token]) throw new Error('Invalid or expired token.');
|
|
117
|
+
|
|
118
|
+
delete privateChats[token];
|
|
119
|
+
delete sessions[u];
|
|
120
|
+
|
|
121
|
+
return { message: 'Private chat cleared successfully' };
|
|
122
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface DiceResult {
|
|
2
|
+
roll: string;
|
|
3
|
+
count: number;
|
|
4
|
+
sides: number;
|
|
5
|
+
modifier: number;
|
|
6
|
+
results: number[];
|
|
7
|
+
total: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function dice(roll: string): DiceResult {
|
|
11
|
+
if (!roll) throw new Error('A roll notation is required (e.g. 2d6+3)');
|
|
12
|
+
if (typeof roll !== 'string') throw new Error('Roll must be a string');
|
|
13
|
+
|
|
14
|
+
const match = /^(\d*)d(\d+)([+-]\d+)?$/i.exec(roll.trim().replace(/\s+/g, '+'));
|
|
15
|
+
if (!match) throw new Error('Invalid notation. Use NdX or NdX+M (e.g. 2d6+3)');
|
|
16
|
+
|
|
17
|
+
const count = match[1] ? parseInt(match[1], 10) : 1;
|
|
18
|
+
const sides = parseInt(match[2]!, 10);
|
|
19
|
+
const modifier = match[3] ? parseInt(match[3], 10) : 0;
|
|
20
|
+
|
|
21
|
+
if (count < 1 || count > 100) throw new Error('Number of dice must be between 1 and 100');
|
|
22
|
+
if (sides < 2 || sides > 1000) throw new Error('Number of sides must be between 2 and 1000');
|
|
23
|
+
|
|
24
|
+
const results: number[] = [];
|
|
25
|
+
for (let i = 0; i < count; i++) {
|
|
26
|
+
results.push(1 + Math.floor(Math.random() * sides));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const total = results.reduce((a, b) => a + b, 0) + modifier;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
roll: `${count}d${sides}${modifier ? (modifier > 0 ? `+${modifier}` : modifier) : ''}`,
|
|
33
|
+
count,
|
|
34
|
+
sides,
|
|
35
|
+
modifier,
|
|
36
|
+
results,
|
|
37
|
+
total,
|
|
38
|
+
};
|
|
39
|
+
}
|