@20syldev/api 4.6.0 → 4.8.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 +5 -3
- package/dist/app.js +6 -6
- package/dist/app.js.map +1 -1
- package/dist/config/env.js.map +1 -1
- package/dist/config/versions.js +22 -3
- package/dist/config/versions.js.map +1 -1
- package/dist/constants.js +27 -12
- package/dist/constants.js.map +1 -1
- package/dist/middleware/error.js +2 -2
- package/dist/middleware/error.js.map +1 -1
- package/dist/middleware/logger.js.map +1 -1
- package/dist/middleware/ratelimit.js +2 -2
- package/dist/middleware/ratelimit.js.map +1 -1
- package/dist/middleware/version.js.map +1 -1
- package/dist/modules/v3.js +1 -1
- package/dist/modules/v3.js.map +1 -1
- package/dist/modules/v4/address.js +381 -0
- package/dist/modules/v4/address.js.map +1 -0
- package/dist/modules/v4/agent.js +92 -0
- package/dist/modules/v4/agent.js.map +1 -0
- package/dist/modules/v4/algorithms.js +79 -0
- package/dist/modules/v4/algorithms.js.map +1 -1
- package/dist/modules/v4/avatar.js +136 -0
- package/dist/modules/v4/avatar.js.map +1 -0
- package/dist/modules/v4/barcode.js +440 -0
- package/dist/modules/v4/barcode.js.map +1 -0
- package/dist/modules/v4/captcha.js +7 -0
- package/dist/modules/v4/captcha.js.map +1 -1
- package/dist/modules/v4/chat.js +9 -1
- package/dist/modules/v4/chat.js.map +1 -1
- package/dist/modules/v4/color.js +8 -1
- package/dist/modules/v4/color.js.map +1 -1
- package/dist/modules/v4/convert.js +9 -0
- package/dist/modules/v4/convert.js.map +1 -1
- package/dist/modules/v4/credit.js +104 -0
- package/dist/modules/v4/credit.js.map +1 -0
- package/dist/modules/v4/cron.js +202 -0
- package/dist/modules/v4/cron.js.map +1 -0
- package/dist/modules/v4/dice.js +7 -0
- package/dist/modules/v4/dice.js.map +1 -1
- package/dist/modules/v4/domain.js +6 -1
- package/dist/modules/v4/domain.js.map +1 -1
- package/dist/modules/v4/encode.js +71 -0
- package/dist/modules/v4/encode.js.map +1 -1
- package/dist/modules/v4/geo.js +11 -1
- package/dist/modules/v4/geo.js.map +1 -1
- package/dist/modules/v4/hash.js +10 -1
- package/dist/modules/v4/hash.js.map +1 -1
- package/dist/modules/v4/hyperplanning.js +9 -1
- package/dist/modules/v4/hyperplanning.js.map +1 -1
- package/dist/modules/v4/ip.js +132 -0
- package/dist/modules/v4/ip.js.map +1 -0
- package/dist/modules/v4/levenshtein.js +8 -0
- package/dist/modules/v4/levenshtein.js.map +1 -1
- package/dist/modules/v4/palette.js +9 -1
- package/dist/modules/v4/palette.js.map +1 -1
- package/dist/modules/v4/password.js +343 -0
- package/dist/modules/v4/password.js.map +1 -0
- package/dist/modules/v4/personal.js +14 -18
- package/dist/modules/v4/personal.js.map +1 -1
- package/dist/modules/v4/placeholder.js +8 -0
- package/dist/modules/v4/placeholder.js.map +1 -1
- package/dist/modules/v4/qrcode.js +10 -3
- package/dist/modules/v4/qrcode.js.map +1 -1
- package/dist/modules/v4/regex.js +48 -0
- package/dist/modules/v4/regex.js.map +1 -0
- package/dist/modules/v4/statistics.js +7 -0
- package/dist/modules/v4/statistics.js.map +1 -1
- package/dist/modules/v4/text.js +30 -0
- package/dist/modules/v4/text.js.map +1 -1
- package/dist/modules/v4/tic_tac_toe.js +9 -1
- package/dist/modules/v4/tic_tac_toe.js.map +1 -1
- package/dist/modules/v4/time.js +62 -11
- package/dist/modules/v4/time.js.map +1 -1
- package/dist/modules/v4/token.js +17 -8
- package/dist/modules/v4/token.js.map +1 -1
- package/dist/modules/v4/username.js +5 -0
- package/dist/modules/v4/username.js.map +1 -1
- package/dist/modules/v4/validate.js +21 -0
- package/dist/modules/v4/validate.js.map +1 -1
- package/dist/modules/v4.js +10 -1
- package/dist/modules/v4.js.map +1 -1
- package/dist/routes/delete.js.map +1 -1
- package/dist/routes/get.js +208 -48
- package/dist/routes/get.js.map +1 -1
- package/dist/routes/index.js +2 -2
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/patch.js.map +1 -1
- package/dist/routes/post.js +2 -2
- package/dist/routes/post.js.map +1 -1
- package/dist/utils/response.js.map +1 -1
- package/package.json +1 -1
- package/src/config/versions.ts +8 -1
- package/src/constants.ts +27 -12
- package/src/modules/v4/address.ts +412 -0
- package/src/modules/v4/cron.ts +221 -0
- package/src/modules/v4/geo.ts +2 -1
- package/src/modules/v4/password.ts +379 -0
- package/src/modules/v4/personal.ts +4 -16
- package/src/modules/v4/qrcode.ts +2 -2
- package/src/modules/v4/regex.ts +67 -0
- package/src/modules/v4/time.ts +63 -17
- package/src/modules/v4/token.ts +8 -7
- package/src/modules/v4.ts +4 -0
- package/src/routes/get.ts +125 -46
- package/tests/integration/api.test.ts +111 -0
- package/tests/unit/address.test.ts +84 -0
- package/tests/unit/cron.test.ts +110 -0
- package/tests/unit/password.test.ts +86 -0
- package/tests/unit/regex.test.ts +95 -0
- package/tests/unit/time.test.ts +47 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { MAX_ADDRESS_COUNT } from '../../constants.js';
|
|
2
|
+
import { random, randomNumber } from '../../utils/helpers.js';
|
|
3
|
+
|
|
4
|
+
interface CountryData {
|
|
5
|
+
name: string;
|
|
6
|
+
streetTypes: string[];
|
|
7
|
+
streetNames: string[];
|
|
8
|
+
cities: string[];
|
|
9
|
+
regions: string[];
|
|
10
|
+
zipFormat: () => string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function randomChar(chars: string): string {
|
|
14
|
+
return chars[Math.floor(Math.random() * chars.length)]!;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
18
|
+
|
|
19
|
+
const countries: Record<string, CountryData> = {
|
|
20
|
+
fr: {
|
|
21
|
+
name: 'France',
|
|
22
|
+
streetTypes: ['Rue', 'Avenue', 'Boulevard', 'Allée', 'Impasse', 'Place', 'Chemin', 'Route', 'Voie', 'Passage'],
|
|
23
|
+
streetNames: [
|
|
24
|
+
'de la Paix',
|
|
25
|
+
'Victor Hugo',
|
|
26
|
+
'Jean Jaurès',
|
|
27
|
+
'de la République',
|
|
28
|
+
'du Général de Gaulle',
|
|
29
|
+
'de la Liberté',
|
|
30
|
+
'des Fleurs',
|
|
31
|
+
'du Moulin',
|
|
32
|
+
'de la Forêt',
|
|
33
|
+
'des Lilas',
|
|
34
|
+
'du Château',
|
|
35
|
+
'de la Mairie',
|
|
36
|
+
'de la Gare',
|
|
37
|
+
'du Commerce',
|
|
38
|
+
'de la Fontaine',
|
|
39
|
+
'de la Croix',
|
|
40
|
+
'des Vignes',
|
|
41
|
+
'du Marché',
|
|
42
|
+
"de l'Église",
|
|
43
|
+
'des Acacias',
|
|
44
|
+
],
|
|
45
|
+
cities: [
|
|
46
|
+
'Paris',
|
|
47
|
+
'Lyon',
|
|
48
|
+
'Marseille',
|
|
49
|
+
'Toulouse',
|
|
50
|
+
'Nice',
|
|
51
|
+
'Nantes',
|
|
52
|
+
'Strasbourg',
|
|
53
|
+
'Montpellier',
|
|
54
|
+
'Bordeaux',
|
|
55
|
+
'Lille',
|
|
56
|
+
'Rennes',
|
|
57
|
+
'Reims',
|
|
58
|
+
'Le Havre',
|
|
59
|
+
'Saint-Étienne',
|
|
60
|
+
'Toulon',
|
|
61
|
+
'Grenoble',
|
|
62
|
+
'Dijon',
|
|
63
|
+
'Angers',
|
|
64
|
+
'Nîmes',
|
|
65
|
+
'Villeurbanne',
|
|
66
|
+
],
|
|
67
|
+
regions: [
|
|
68
|
+
'Île-de-France',
|
|
69
|
+
'Auvergne-Rhône-Alpes',
|
|
70
|
+
"Provence-Alpes-Côte d'Azur",
|
|
71
|
+
'Occitanie',
|
|
72
|
+
'Hauts-de-France',
|
|
73
|
+
'Nouvelle-Aquitaine',
|
|
74
|
+
'Grand Est',
|
|
75
|
+
'Pays de la Loire',
|
|
76
|
+
'Bretagne',
|
|
77
|
+
'Normandie',
|
|
78
|
+
'Bourgogne-Franche-Comté',
|
|
79
|
+
'Centre-Val de Loire',
|
|
80
|
+
],
|
|
81
|
+
zipFormat: () => String(randomNumber(10000, 99999)).padStart(5, '0'),
|
|
82
|
+
},
|
|
83
|
+
us: {
|
|
84
|
+
name: 'United States',
|
|
85
|
+
streetTypes: ['Street', 'Avenue', 'Boulevard', 'Drive', 'Road', 'Lane', 'Court', 'Way', 'Place', 'Circle'],
|
|
86
|
+
streetNames: [
|
|
87
|
+
'Main',
|
|
88
|
+
'Oak',
|
|
89
|
+
'Maple',
|
|
90
|
+
'Cedar',
|
|
91
|
+
'Pine',
|
|
92
|
+
'Elm',
|
|
93
|
+
'Washington',
|
|
94
|
+
'Lincoln',
|
|
95
|
+
'Park',
|
|
96
|
+
'Lake',
|
|
97
|
+
'Hill',
|
|
98
|
+
'River',
|
|
99
|
+
'Sunset',
|
|
100
|
+
'Forest',
|
|
101
|
+
'Meadow',
|
|
102
|
+
'Valley',
|
|
103
|
+
'Highland',
|
|
104
|
+
'Willow',
|
|
105
|
+
'Spring',
|
|
106
|
+
'Church',
|
|
107
|
+
],
|
|
108
|
+
cities: [
|
|
109
|
+
'New York',
|
|
110
|
+
'Los Angeles',
|
|
111
|
+
'Chicago',
|
|
112
|
+
'Houston',
|
|
113
|
+
'Phoenix',
|
|
114
|
+
'Philadelphia',
|
|
115
|
+
'San Antonio',
|
|
116
|
+
'San Diego',
|
|
117
|
+
'Dallas',
|
|
118
|
+
'San Jose',
|
|
119
|
+
'Austin',
|
|
120
|
+
'Jacksonville',
|
|
121
|
+
'Fort Worth',
|
|
122
|
+
'Columbus',
|
|
123
|
+
'Charlotte',
|
|
124
|
+
'Indianapolis',
|
|
125
|
+
'San Francisco',
|
|
126
|
+
'Seattle',
|
|
127
|
+
'Denver',
|
|
128
|
+
'Nashville',
|
|
129
|
+
],
|
|
130
|
+
regions: [
|
|
131
|
+
'California',
|
|
132
|
+
'Texas',
|
|
133
|
+
'Florida',
|
|
134
|
+
'New York',
|
|
135
|
+
'Pennsylvania',
|
|
136
|
+
'Illinois',
|
|
137
|
+
'Ohio',
|
|
138
|
+
'Georgia',
|
|
139
|
+
'North Carolina',
|
|
140
|
+
'Michigan',
|
|
141
|
+
'New Jersey',
|
|
142
|
+
'Virginia',
|
|
143
|
+
'Washington',
|
|
144
|
+
'Arizona',
|
|
145
|
+
'Massachusetts',
|
|
146
|
+
'Tennessee',
|
|
147
|
+
'Indiana',
|
|
148
|
+
'Colorado',
|
|
149
|
+
'Missouri',
|
|
150
|
+
'Maryland',
|
|
151
|
+
],
|
|
152
|
+
zipFormat: () => String(randomNumber(10000, 99999)).padStart(5, '0'),
|
|
153
|
+
},
|
|
154
|
+
uk: {
|
|
155
|
+
name: 'United Kingdom',
|
|
156
|
+
streetTypes: ['Street', 'Road', 'Avenue', 'Lane', 'Close', 'Drive', 'Way', 'Court', 'Grove', 'Place'],
|
|
157
|
+
streetNames: [
|
|
158
|
+
'High',
|
|
159
|
+
'Church',
|
|
160
|
+
'Station',
|
|
161
|
+
'Park',
|
|
162
|
+
'Manor',
|
|
163
|
+
'Mill',
|
|
164
|
+
'Victoria',
|
|
165
|
+
'King',
|
|
166
|
+
'Queen',
|
|
167
|
+
'Albert',
|
|
168
|
+
'George',
|
|
169
|
+
'York',
|
|
170
|
+
'Oxford',
|
|
171
|
+
'Cambridge',
|
|
172
|
+
'Windsor',
|
|
173
|
+
'Richmond',
|
|
174
|
+
'Elm',
|
|
175
|
+
'Oak',
|
|
176
|
+
'Ash',
|
|
177
|
+
'Rose',
|
|
178
|
+
],
|
|
179
|
+
cities: [
|
|
180
|
+
'London',
|
|
181
|
+
'Birmingham',
|
|
182
|
+
'Manchester',
|
|
183
|
+
'Glasgow',
|
|
184
|
+
'Liverpool',
|
|
185
|
+
'Bristol',
|
|
186
|
+
'Sheffield',
|
|
187
|
+
'Leeds',
|
|
188
|
+
'Edinburgh',
|
|
189
|
+
'Leicester',
|
|
190
|
+
'Coventry',
|
|
191
|
+
'Bradford',
|
|
192
|
+
'Cardiff',
|
|
193
|
+
'Belfast',
|
|
194
|
+
'Nottingham',
|
|
195
|
+
'Kingston upon Hull',
|
|
196
|
+
'Newcastle',
|
|
197
|
+
'Stoke-on-Trent',
|
|
198
|
+
'Southampton',
|
|
199
|
+
'Derby',
|
|
200
|
+
],
|
|
201
|
+
regions: [
|
|
202
|
+
'England',
|
|
203
|
+
'Scotland',
|
|
204
|
+
'Wales',
|
|
205
|
+
'Northern Ireland',
|
|
206
|
+
'Greater London',
|
|
207
|
+
'South East',
|
|
208
|
+
'North West',
|
|
209
|
+
'East of England',
|
|
210
|
+
'West Midlands',
|
|
211
|
+
'South West',
|
|
212
|
+
'Yorkshire',
|
|
213
|
+
'East Midlands',
|
|
214
|
+
'North East',
|
|
215
|
+
],
|
|
216
|
+
zipFormat: () =>
|
|
217
|
+
`${randomChar(ALPHA)}${randomChar(ALPHA)}${randomNumber(1, 9)} ${randomNumber(1, 9)}${randomChar(ALPHA)}${randomChar(ALPHA)}`,
|
|
218
|
+
},
|
|
219
|
+
de: {
|
|
220
|
+
name: 'Germany',
|
|
221
|
+
streetTypes: ['Straße', 'Allee', 'Weg', 'Platz', 'Gasse', 'Ring', 'Damm', 'Ufer', 'Stieg', 'Pfad'],
|
|
222
|
+
streetNames: [
|
|
223
|
+
'Haupt',
|
|
224
|
+
'Bahnhof',
|
|
225
|
+
'Schul',
|
|
226
|
+
'Kirch',
|
|
227
|
+
'Garten',
|
|
228
|
+
'Berg',
|
|
229
|
+
'Wald',
|
|
230
|
+
'Wiesen',
|
|
231
|
+
'Tal',
|
|
232
|
+
'See',
|
|
233
|
+
'Bismarck',
|
|
234
|
+
'Goethe',
|
|
235
|
+
'Schiller',
|
|
236
|
+
'Kant',
|
|
237
|
+
'Hegel',
|
|
238
|
+
'Mozart',
|
|
239
|
+
'Beethoven',
|
|
240
|
+
'Bach',
|
|
241
|
+
'Brahms',
|
|
242
|
+
'Handel',
|
|
243
|
+
],
|
|
244
|
+
cities: [
|
|
245
|
+
'Berlin',
|
|
246
|
+
'Hamburg',
|
|
247
|
+
'Munich',
|
|
248
|
+
'Cologne',
|
|
249
|
+
'Frankfurt',
|
|
250
|
+
'Stuttgart',
|
|
251
|
+
'Düsseldorf',
|
|
252
|
+
'Leipzig',
|
|
253
|
+
'Dortmund',
|
|
254
|
+
'Essen',
|
|
255
|
+
'Bremen',
|
|
256
|
+
'Dresden',
|
|
257
|
+
'Hanover',
|
|
258
|
+
'Nuremberg',
|
|
259
|
+
'Duisburg',
|
|
260
|
+
'Bochum',
|
|
261
|
+
'Wuppertal',
|
|
262
|
+
'Bielefeld',
|
|
263
|
+
'Bonn',
|
|
264
|
+
'Münster',
|
|
265
|
+
],
|
|
266
|
+
regions: [
|
|
267
|
+
'Bayern',
|
|
268
|
+
'Nordrhein-Westfalen',
|
|
269
|
+
'Baden-Württemberg',
|
|
270
|
+
'Niedersachsen',
|
|
271
|
+
'Hessen',
|
|
272
|
+
'Sachsen',
|
|
273
|
+
'Rheinland-Pfalz',
|
|
274
|
+
'Berlin',
|
|
275
|
+
'Schleswig-Holstein',
|
|
276
|
+
'Brandenburg',
|
|
277
|
+
'Sachsen-Anhalt',
|
|
278
|
+
'Thüringen',
|
|
279
|
+
'Hamburg',
|
|
280
|
+
'Mecklenburg-Vorpommern',
|
|
281
|
+
'Bremen',
|
|
282
|
+
'Saarland',
|
|
283
|
+
],
|
|
284
|
+
zipFormat: () => String(randomNumber(10000, 99999)).padStart(5, '0'),
|
|
285
|
+
},
|
|
286
|
+
es: {
|
|
287
|
+
name: 'Spain',
|
|
288
|
+
streetTypes: [
|
|
289
|
+
'Calle',
|
|
290
|
+
'Avenida',
|
|
291
|
+
'Plaza',
|
|
292
|
+
'Paseo',
|
|
293
|
+
'Rambla',
|
|
294
|
+
'Carretera',
|
|
295
|
+
'Camino',
|
|
296
|
+
'Vía',
|
|
297
|
+
'Travesía',
|
|
298
|
+
'Callejón',
|
|
299
|
+
],
|
|
300
|
+
streetNames: [
|
|
301
|
+
'Mayor',
|
|
302
|
+
'Real',
|
|
303
|
+
'Nueva',
|
|
304
|
+
'del Sol',
|
|
305
|
+
'de la Luna',
|
|
306
|
+
'del Rey',
|
|
307
|
+
'de la Reina',
|
|
308
|
+
'del Mar',
|
|
309
|
+
'de la Paz',
|
|
310
|
+
'de la Victoria',
|
|
311
|
+
'Cervantes',
|
|
312
|
+
'Goya',
|
|
313
|
+
'Velázquez',
|
|
314
|
+
'Picasso',
|
|
315
|
+
'Dalí',
|
|
316
|
+
'de España',
|
|
317
|
+
'de Colón',
|
|
318
|
+
'de la Constitución',
|
|
319
|
+
'del Prado',
|
|
320
|
+
'de las Flores',
|
|
321
|
+
],
|
|
322
|
+
cities: [
|
|
323
|
+
'Madrid',
|
|
324
|
+
'Barcelona',
|
|
325
|
+
'Valencia',
|
|
326
|
+
'Seville',
|
|
327
|
+
'Zaragoza',
|
|
328
|
+
'Málaga',
|
|
329
|
+
'Murcia',
|
|
330
|
+
'Palma',
|
|
331
|
+
'Las Palmas',
|
|
332
|
+
'Bilbao',
|
|
333
|
+
'Alicante',
|
|
334
|
+
'Córdoba',
|
|
335
|
+
'Valladolid',
|
|
336
|
+
'Vigo',
|
|
337
|
+
'Gijón',
|
|
338
|
+
'Hospitalet',
|
|
339
|
+
'A Coruña',
|
|
340
|
+
'Granada',
|
|
341
|
+
'Vitoria',
|
|
342
|
+
'Elche',
|
|
343
|
+
],
|
|
344
|
+
regions: [
|
|
345
|
+
'Andalucía',
|
|
346
|
+
'Cataluña',
|
|
347
|
+
'Madrid',
|
|
348
|
+
'Comunidad Valenciana',
|
|
349
|
+
'Galicia',
|
|
350
|
+
'Castilla y León',
|
|
351
|
+
'País Vasco',
|
|
352
|
+
'Castilla-La Mancha',
|
|
353
|
+
'Canarias',
|
|
354
|
+
'Murcia',
|
|
355
|
+
'Aragón',
|
|
356
|
+
'Extremadura',
|
|
357
|
+
'Asturias',
|
|
358
|
+
'Navarra',
|
|
359
|
+
'Cantabria',
|
|
360
|
+
'La Rioja',
|
|
361
|
+
'Baleares',
|
|
362
|
+
],
|
|
363
|
+
zipFormat: () => String(randomNumber(10000, 52999)).padStart(5, '0'),
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const COUNTRY_KEYS = Object.keys(countries);
|
|
368
|
+
|
|
369
|
+
export interface Address {
|
|
370
|
+
street: string;
|
|
371
|
+
city: string;
|
|
372
|
+
zip: string;
|
|
373
|
+
state: string;
|
|
374
|
+
country: string;
|
|
375
|
+
countryCode: string;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export interface AddressResult {
|
|
379
|
+
addresses: Address[];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Generates one or more fictional postal addresses for a given country.
|
|
384
|
+
*
|
|
385
|
+
* @param countryCode - Country code: "fr", "us", "uk", "de", or "es" (random if omitted)
|
|
386
|
+
* @param count - Number of addresses to generate (1–10)
|
|
387
|
+
* @returns Array of addresses with street, city, zip, state, country, and countryCode
|
|
388
|
+
* @throws Error if the country code is invalid or count is out of range
|
|
389
|
+
*/
|
|
390
|
+
export default function address(countryCode?: string, count: number = 1): AddressResult {
|
|
391
|
+
if (count < 1 || count > MAX_ADDRESS_COUNT) {
|
|
392
|
+
throw new Error(`Count must be between 1 and ${MAX_ADDRESS_COUNT}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const code = (countryCode ?? random(COUNTRY_KEYS)).toLowerCase();
|
|
396
|
+
if (!countries[code]) {
|
|
397
|
+
throw new Error(`Unknown country code "${code}". Supported: ${COUNTRY_KEYS.join(', ')}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const data = countries[code]!;
|
|
401
|
+
|
|
402
|
+
const addresses: Address[] = Array.from({ length: count }, () => ({
|
|
403
|
+
street: `${randomNumber(1, 200)} ${random(data.streetTypes)} ${random(data.streetNames)}`,
|
|
404
|
+
city: random(data.cities),
|
|
405
|
+
zip: data.zipFormat(),
|
|
406
|
+
state: random(data.regions),
|
|
407
|
+
country: data.name,
|
|
408
|
+
countryCode: code.toUpperCase(),
|
|
409
|
+
}));
|
|
410
|
+
|
|
411
|
+
return { addresses };
|
|
412
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { MAX_CRON_ITERATIONS, MAX_CRON_RESULTS } from '../../constants.js';
|
|
2
|
+
import { VALID_TIMEZONES } from './time.js';
|
|
3
|
+
|
|
4
|
+
type Timezone = (typeof VALID_TIMEZONES)[number];
|
|
5
|
+
|
|
6
|
+
const FIELD_RANGES = [
|
|
7
|
+
{ min: 0, max: 59 }, // minute
|
|
8
|
+
{ min: 0, max: 23 }, // hour
|
|
9
|
+
{ min: 1, max: 31 }, // day of month
|
|
10
|
+
{ min: 1, max: 12 }, // month
|
|
11
|
+
{ min: 0, max: 7 }, // day of week (0 and 7 = Sunday)
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
const DOW_NAMES: Record<string, number> = {
|
|
15
|
+
Sunday: 0,
|
|
16
|
+
Monday: 1,
|
|
17
|
+
Tuesday: 2,
|
|
18
|
+
Wednesday: 3,
|
|
19
|
+
Thursday: 4,
|
|
20
|
+
Friday: 5,
|
|
21
|
+
Saturday: 6,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const DOW_DISPLAY = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
25
|
+
|
|
26
|
+
export interface CronResult {
|
|
27
|
+
expression: string;
|
|
28
|
+
description: string;
|
|
29
|
+
next: string[];
|
|
30
|
+
timezone: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseField(field: string, range: { min: number; max: number }): Set<number> {
|
|
34
|
+
const result = new Set<number>();
|
|
35
|
+
|
|
36
|
+
for (const part of field.split(',')) {
|
|
37
|
+
if (part === '*') {
|
|
38
|
+
for (let i = range.min; i <= range.max; i++) result.add(i);
|
|
39
|
+
} else if (part.startsWith('*/')) {
|
|
40
|
+
const step = parseInt(part.slice(2), 10);
|
|
41
|
+
if (isNaN(step) || step < 1) throw new Error(`Invalid step in "${part}"`);
|
|
42
|
+
for (let i = range.min; i <= range.max; i += step) result.add(i);
|
|
43
|
+
} else if (part.includes('-')) {
|
|
44
|
+
const [s, e] = part.split('-');
|
|
45
|
+
const start = parseInt(s!, 10);
|
|
46
|
+
const end = parseInt(e!, 10);
|
|
47
|
+
if (isNaN(start) || isNaN(end) || start < range.min || end > range.max || start > end) {
|
|
48
|
+
throw new Error(`Invalid range "${part}" (valid: ${range.min}-${range.max})`);
|
|
49
|
+
}
|
|
50
|
+
for (let i = start; i <= end; i++) result.add(i);
|
|
51
|
+
} else {
|
|
52
|
+
const val = parseInt(part, 10);
|
|
53
|
+
if (isNaN(val) || val < range.min || val > range.max) {
|
|
54
|
+
throw new Error(`Invalid value "${part}" (valid: ${range.min}-${range.max})`);
|
|
55
|
+
}
|
|
56
|
+
result.add(val);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getComponents(
|
|
64
|
+
date: Date,
|
|
65
|
+
formatter: Intl.DateTimeFormat,
|
|
66
|
+
): { minute: number; hour: number; dom: number; month: number; dow: number } {
|
|
67
|
+
const parts = Object.fromEntries(formatter.formatToParts(date).map((p) => [p.type, p.value]));
|
|
68
|
+
return {
|
|
69
|
+
minute: parseInt(parts.minute!, 10),
|
|
70
|
+
hour: parseInt(parts.hour!, 10) % 24,
|
|
71
|
+
dom: parseInt(parts.day!, 10),
|
|
72
|
+
month: parseInt(parts.month!, 10),
|
|
73
|
+
dow: DOW_NAMES[parts.weekday!] ?? 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function pad(n: number): string {
|
|
78
|
+
return String(n).padStart(2, '0');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatDow(field: string): string {
|
|
82
|
+
if (field === '1-5') return 'Monday through Friday';
|
|
83
|
+
if (field === '0' || field === '7') return 'Sunday';
|
|
84
|
+
if (field === '6') return 'Saturday';
|
|
85
|
+
const n = parseInt(field, 10);
|
|
86
|
+
if (!isNaN(n)) return DOW_DISPLAY[n % 7] ?? field;
|
|
87
|
+
return field;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function describe(fields: string[]): string {
|
|
91
|
+
const [mF, hF, domF, monthF, dowF] = fields as [string, string, string, string, string];
|
|
92
|
+
|
|
93
|
+
const isAll = (f: string) => f === '*';
|
|
94
|
+
const isFixed = (f: string) => /^\d+$/.test(f);
|
|
95
|
+
const isStep = (f: string) => /^\*\/\d+$/.test(f);
|
|
96
|
+
|
|
97
|
+
// Every minute
|
|
98
|
+
if (isAll(mF) && isAll(hF) && isAll(domF) && isAll(monthF) && isAll(dowF)) {
|
|
99
|
+
return 'Every minute';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Every N minutes (whole day, every day)
|
|
103
|
+
if (isStep(mF) && isAll(hF) && isAll(domF) && isAll(monthF) && isAll(dowF)) {
|
|
104
|
+
const n = mF.slice(2);
|
|
105
|
+
return `Every ${n} minute${n === '1' ? '' : 's'}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Every hour at minute 0
|
|
109
|
+
if (mF === '0' && isAll(hF) && isAll(domF) && isAll(monthF) && isAll(dowF)) {
|
|
110
|
+
return 'Every hour';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Every N hours at minute 0
|
|
114
|
+
if (mF === '0' && isStep(hF) && isAll(domF) && isAll(monthF) && isAll(dowF)) {
|
|
115
|
+
const n = hF.slice(2);
|
|
116
|
+
return `Every ${n} hour${n === '1' ? '' : 's'}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// At specific time daily
|
|
120
|
+
if (isFixed(mF) && isFixed(hF) && isAll(domF) && isAll(monthF) && isAll(dowF)) {
|
|
121
|
+
return `At ${pad(parseInt(hF))}:${pad(parseInt(mF))} daily`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// At specific time on specific weekday(s)
|
|
125
|
+
if (isFixed(mF) && isFixed(hF) && isAll(domF) && isAll(monthF) && !isAll(dowF)) {
|
|
126
|
+
return `At ${pad(parseInt(hF))}:${pad(parseInt(mF))}, ${formatDow(dowF)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// At specific time on specific day of month
|
|
130
|
+
if (isFixed(mF) && isFixed(hF) && isFixed(domF) && isAll(monthF) && isAll(dowF)) {
|
|
131
|
+
return `At ${pad(parseInt(hF))}:${pad(parseInt(mF))}, on day ${domF} of the month`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Every N minutes, within hour range, optionally filtered by weekday
|
|
135
|
+
if (isStep(mF) && !isAll(hF) && isAll(domF) && isAll(monthF)) {
|
|
136
|
+
const n = mF.slice(2);
|
|
137
|
+
const parts = [`Every ${n} minute${n === '1' ? '' : 's'}`];
|
|
138
|
+
if (hF.includes('-')) {
|
|
139
|
+
const [h1, h2] = hF.split('-');
|
|
140
|
+
parts.push(`between ${pad(parseInt(h1!))}:00 and ${pad(parseInt(h2!))}:59`);
|
|
141
|
+
}
|
|
142
|
+
if (!isAll(dowF)) parts.push(formatDow(dowF));
|
|
143
|
+
return parts.join(', ');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `At cron schedule: ${fields.join(' ')}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parses a cron expression and returns the next scheduled execution dates.
|
|
151
|
+
*
|
|
152
|
+
* @param expr - Cron expression with 5 space-separated fields (minute hour dom month dow)
|
|
153
|
+
* @param count - Number of next executions to return (1–20, default 5)
|
|
154
|
+
* @param from - Starting date in ISO 8601 format (defaults to now)
|
|
155
|
+
* @param timezone - Timezone to evaluate the expression in (default "UTC")
|
|
156
|
+
* @returns Parsed expression with description and list of next execution timestamps
|
|
157
|
+
* @throws Error if the expression is invalid, count is out of range, or timezone is unsupported
|
|
158
|
+
*/
|
|
159
|
+
export default function cron(expr: string, count: number = 5, from?: string, timezone: string = 'UTC'): CronResult {
|
|
160
|
+
if (!expr) throw new Error('Please provide a cron expression (?expr=* * * * *)');
|
|
161
|
+
|
|
162
|
+
if (!VALID_TIMEZONES.includes(timezone as Timezone)) {
|
|
163
|
+
throw new Error(`Please provide a valid timezone. Options: ${VALID_TIMEZONES.join(', ')}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const fields = expr.trim().split(/\s+/);
|
|
167
|
+
if (fields.length !== 5) {
|
|
168
|
+
throw new Error('Cron expression must have exactly 5 fields (minute hour dom month dow)');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (count < 1 || count > MAX_CRON_RESULTS) {
|
|
172
|
+
throw new Error(`Count must be between 1 and ${MAX_CRON_RESULTS}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const fromMs = from ? new Date(from).getTime() : Date.now();
|
|
176
|
+
if (from && isNaN(fromMs)) throw new Error('Please provide a valid from date (ISO 8601)');
|
|
177
|
+
|
|
178
|
+
const sets = fields.map((f, i) => parseField(f, FIELD_RANGES[i]!));
|
|
179
|
+
const [minuteSet, hourSet, domSet, monthSet, dowSet] = sets as [
|
|
180
|
+
Set<number>,
|
|
181
|
+
Set<number>,
|
|
182
|
+
Set<number>,
|
|
183
|
+
Set<number>,
|
|
184
|
+
Set<number>,
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
// Normalize DOW: treat 7 as Sunday (0)
|
|
188
|
+
if (dowSet.has(7)) {
|
|
189
|
+
dowSet.add(0);
|
|
190
|
+
dowSet.delete(7);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
194
|
+
timeZone: timezone,
|
|
195
|
+
month: 'numeric',
|
|
196
|
+
day: 'numeric',
|
|
197
|
+
hour: 'numeric',
|
|
198
|
+
minute: 'numeric',
|
|
199
|
+
weekday: 'long',
|
|
200
|
+
hour12: false,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Always start from the next minute (never include the current minute)
|
|
204
|
+
let currentMs = (Math.floor(fromMs / 60000) + 1) * 60000;
|
|
205
|
+
|
|
206
|
+
const next: string[] = [];
|
|
207
|
+
let iterations = 0;
|
|
208
|
+
|
|
209
|
+
while (next.length < count && iterations < MAX_CRON_ITERATIONS) {
|
|
210
|
+
const { minute, hour, dom, month, dow } = getComponents(new Date(currentMs), formatter);
|
|
211
|
+
|
|
212
|
+
if (minuteSet.has(minute) && hourSet.has(hour) && domSet.has(dom) && monthSet.has(month) && dowSet.has(dow)) {
|
|
213
|
+
next.push(new Date(currentMs).toISOString());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
currentMs += 60000;
|
|
217
|
+
iterations++;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { expression: expr.trim(), description: describe(fields), next, timezone };
|
|
221
|
+
}
|
package/src/modules/v4/geo.ts
CHANGED
|
@@ -5,7 +5,8 @@ export interface GeoResult {
|
|
|
5
5
|
to: { lat: number; lon: number };
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import { EARTH_RADIUS_KM } from '../../constants.js';
|
|
9
|
+
|
|
9
10
|
const CARDINALS = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
|
|
10
11
|
|
|
11
12
|
function toRad(deg: number): number {
|