@mhmdhammoud/meritt-utils 1.6.2 → 1.6.3

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.
@@ -1,356 +0,0 @@
1
- /**
2
- * Elasticsearch transport for Pino with connection lifecycle resilience.
3
- *
4
- * Based on pino-elasticsearch with a fix for GitHub issue #140:
5
- * When maxRetries are exceeded and Elasticsearch nodes are DEAD, the bulk helper
6
- * destroys the splitter stream, causing logs to stop permanently until restart.
7
- *
8
- * This implementation overrides splitter.destroy to BOTH resurrect the connection
9
- * pool AND reinitialize the bulk handler, so logging continues after ES recovers.
10
- *
11
- * @see https://github.com/pinojs/pino-elasticsearch/issues/140
12
- * @see https://github.com/pinojs/pino-elasticsearch/issues/72
13
- */
14
-
15
- // eslint-disable-next-line @typescript-eslint/no-require-imports
16
- const split = require('split2') as (
17
- fn: (line: string) => unknown,
18
- opts?: { autoDestroy?: boolean }
19
- ) => NodeJS.ReadWriteStream
20
- import { Client } from '@elastic/elasticsearch'
21
- import type { ClientOptions } from '@elastic/elasticsearch'
22
-
23
- export interface ElasticTransportOptions extends Pick<
24
- ClientOptions,
25
- | 'node'
26
- | 'auth'
27
- | 'cloud'
28
- | 'caFingerprint'
29
- | 'Connection'
30
- | 'ConnectionPool'
31
- | 'maxRetries'
32
- | 'requestTimeout'
33
- > {
34
- sniffOnConnectionFault?: boolean
35
- index?: string | ((logTime: string) => string)
36
- flushBytes?: number
37
- 'flush-bytes'?: number
38
- flushInterval?: number
39
- 'flush-interval'?: number
40
- esVersion?: number
41
- 'es-version'?: number
42
- rejectUnauthorized?: boolean
43
- tls?: ClientOptions['tls']
44
- }
45
-
46
- interface LogDocument {
47
- time?: string
48
- '@timestamp'?: string
49
- [k: string]: unknown
50
- }
51
-
52
- export interface ElasticTransportStream extends NodeJS.ReadWriteStream {
53
- flush: () => Promise<void>
54
- close: () => Promise<void>
55
- }
56
-
57
- const createTimeoutError = (timeoutMs: number): Error =>
58
- new Error(`Elasticsearch bulk request timed out after ${timeoutMs}ms`)
59
-
60
- function setDateTimeString(value: unknown): string {
61
- if (value !== null && typeof value === 'object' && 'time' in value) {
62
- const t = (value as { time: unknown }).time
63
- if (
64
- (typeof t === 'string' && t.length > 0) ||
65
- (typeof t === 'number' && t >= 0)
66
- ) {
67
- return new Date(t).toISOString()
68
- }
69
- }
70
- return new Date().toISOString()
71
- }
72
-
73
- function getIndexName(
74
- index: string | ((logTime: string) => string),
75
- time: string
76
- ): string {
77
- if (typeof index === 'function') {
78
- return index(time)
79
- }
80
- return index.replace('%{DATE}', time.substring(0, 10))
81
- }
82
-
83
- function createBulkSender(
84
- opts: ElasticTransportOptions,
85
- client: Client,
86
- splitter: NodeJS.ReadWriteStream
87
- ): {
88
- add: (doc: unknown) => void
89
- flush: () => Promise<void>
90
- close: () => Promise<void>
91
- } {
92
- const esVersion = Number(opts.esVersion ?? opts['es-version'] ?? 7)
93
- const index = opts.index ?? 'pino'
94
- const buildIndexName = typeof index === 'function' ? index : null
95
- const opType = esVersion >= 7 ? undefined : undefined
96
- const flushBytes = opts.flushBytes ?? opts['flush-bytes'] ?? 1000
97
- const flushInterval = opts.flushInterval ?? opts['flush-interval'] ?? 3000
98
- const requestTimeout = opts.requestTimeout ?? 30000
99
- const bulkWatchdogTimeout = requestTimeout + 5000
100
-
101
- let buffer: unknown[] = []
102
- let bufferedBytes = 0
103
- let timer: NodeJS.Timeout | undefined
104
- let isFlushing = false
105
- let flushAgain = false
106
-
107
- const indexName = (time = new Date().toISOString()) =>
108
- buildIndexName ? buildIndexName(time) : getIndexName(index as string, time)
109
-
110
- const clearFlushTimer = () => {
111
- if (timer) {
112
- clearTimeout(timer)
113
- timer = undefined
114
- }
115
- }
116
-
117
- const scheduleFlush = () => {
118
- if (timer || buffer.length === 0) {
119
- return
120
- }
121
- timer = setTimeout(() => {
122
- timer = undefined
123
- void flush()
124
- }, flushInterval)
125
- timer.unref?.()
126
- }
127
-
128
- const buildOperation = (doc: unknown): [Record<string, unknown>, unknown] => {
129
- try {
130
- const d = doc as LogDocument
131
- const date = d.time ?? d['@timestamp'] ?? new Date().toISOString()
132
- if (opType === 'create') {
133
- d['@timestamp'] = date
134
- }
135
- return [
136
- {
137
- index: {
138
- _index: indexName(date),
139
- op_type: opType,
140
- },
141
- },
142
- doc,
143
- ]
144
- } catch {
145
- return [
146
- {
147
- index: {
148
- _index: indexName(),
149
- op_type: opType,
150
- },
151
- },
152
- doc,
153
- ]
154
- }
155
- }
156
-
157
- const emitDroppedDocument = (doc: unknown, cause?: unknown) => {
158
- const error = new Error('Dropped document') as Error & {
159
- document: unknown
160
- cause?: unknown
161
- }
162
- error.document = doc
163
- error.cause = cause
164
- splitter.emit('insertError', error)
165
- }
166
-
167
- const emitBulkError = (err: unknown) => {
168
- // Do not emit the standard stream "error" event for retryable bulk
169
- // failures. Some stream consumers treat it as terminal, which can leave
170
- // Pino writing into a poisoned stream while the process continues running.
171
- splitter.emit('bulkError', err)
172
- }
173
-
174
- const bulkWithWatchdog = async (
175
- operations: unknown[]
176
- ): Promise<{
177
- errors?: boolean
178
- items?: Array<Record<string, { error?: unknown }>>
179
- }> => {
180
- let timeout: NodeJS.Timeout | undefined
181
- const bulkPromise = client.bulk({
182
- operations,
183
- refresh: false,
184
- timeout: `${requestTimeout}ms`,
185
- }) as Promise<{
186
- errors?: boolean
187
- items?: Array<Record<string, { error?: unknown }>>
188
- }>
189
-
190
- try {
191
- return await Promise.race([
192
- bulkPromise,
193
- new Promise<never>((_, reject) => {
194
- timeout = setTimeout(() => {
195
- reject(createTimeoutError(bulkWatchdogTimeout))
196
- }, bulkWatchdogTimeout)
197
- timeout.unref?.()
198
- }),
199
- ])
200
- } finally {
201
- if (timeout) {
202
- clearTimeout(timeout)
203
- }
204
- // If the watchdog wins, the original request may reject later. Consume
205
- // that rejection so a stale request cannot crash the app.
206
- bulkPromise.catch(() => undefined)
207
- }
208
- }
209
-
210
- const flush = async (): Promise<void> => {
211
- if (isFlushing) {
212
- flushAgain = true
213
- return
214
- }
215
- clearFlushTimer()
216
- if (buffer.length === 0) {
217
- return
218
- }
219
-
220
- isFlushing = true
221
- const batch = buffer
222
- buffer = []
223
- bufferedBytes = 0
224
-
225
- try {
226
- const operations = batch.flatMap(buildOperation)
227
- const body = await bulkWithWatchdog(operations)
228
- if (body.errors && Array.isArray(body.items)) {
229
- body.items.forEach((item, index) => {
230
- const result = Object.values(item)[0]
231
- if (result?.error) {
232
- emitDroppedDocument(batch[index], result.error)
233
- }
234
- })
235
- }
236
-
237
- splitter.emit('insert', {
238
- successful: batch.length,
239
- failed: body.errors
240
- ? (body.items?.filter((item) => Object.values(item)[0]?.error)
241
- .length ?? 0)
242
- : 0,
243
- })
244
- } catch (err) {
245
- emitBulkError(err)
246
- // Drop the failed batch instead of wedging the stream. The next log line
247
- // creates a fresh bulk request and can recover without a process restart.
248
- batch.forEach((doc) => emitDroppedDocument(doc, err))
249
- } finally {
250
- isFlushing = false
251
- if (flushAgain || buffer.length > 0) {
252
- flushAgain = false
253
- scheduleFlush()
254
- if (bufferedBytes >= flushBytes) {
255
- void flush()
256
- }
257
- }
258
- }
259
- }
260
-
261
- return {
262
- add(doc: unknown) {
263
- buffer.push(doc)
264
- bufferedBytes += Buffer.byteLength(JSON.stringify(doc))
265
- if (bufferedBytes >= flushBytes) {
266
- void flush()
267
- return
268
- }
269
- scheduleFlush()
270
- },
271
- flush,
272
- async close() {
273
- clearFlushTimer()
274
- await flush()
275
- },
276
- }
277
- }
278
-
279
- export const createElasticTransport = (
280
- opts: ElasticTransportOptions = {}
281
- ): ElasticTransportStream => {
282
- const splitter = split(
283
- function (this: NodeJS.ReadWriteStream, line: string) {
284
- let value: unknown
285
-
286
- try {
287
- value = JSON.parse(line) as unknown
288
- } catch (error) {
289
- this.emit('unknown', line, error)
290
- return
291
- }
292
-
293
- if (typeof value === 'boolean') {
294
- this.emit('unknown', line, 'Boolean value ignored')
295
- return
296
- }
297
- if (value === null) {
298
- this.emit('unknown', line, 'Null value ignored')
299
- return
300
- }
301
- if (typeof value !== 'object') {
302
- value = { data: value, time: setDateTimeString(value) }
303
- } else {
304
- const obj = value as Record<string, unknown>
305
- if (obj['@timestamp'] === undefined) {
306
- ;(obj as LogDocument).time = setDateTimeString(obj)
307
- }
308
- }
309
- return value
310
- },
311
- { autoDestroy: true }
312
- )
313
-
314
- const clientOpts: ClientOptions = {
315
- node: opts.node,
316
- auth: opts.auth,
317
- cloud: opts.cloud,
318
- tls: { rejectUnauthorized: opts.rejectUnauthorized, ...opts.tls },
319
- maxRetries: opts.maxRetries,
320
- requestTimeout: opts.requestTimeout,
321
- sniffOnConnectionFault: opts.sniffOnConnectionFault,
322
- }
323
-
324
- if (opts.caFingerprint) {
325
- clientOpts.caFingerprint = opts.caFingerprint
326
- }
327
- if (opts.Connection) {
328
- clientOpts.Connection = opts.Connection
329
- }
330
- if (opts.ConnectionPool) {
331
- clientOpts.ConnectionPool = opts.ConnectionPool
332
- }
333
-
334
- const client = new Client(clientOpts)
335
- const bulkSender = createBulkSender(opts, client, splitter)
336
-
337
- splitter.on('data', (doc) => {
338
- bulkSender.add(doc)
339
- })
340
- splitter.on('finish', () => {
341
- void bulkSender.close()
342
- })
343
-
344
- const splitterWithDestroy = splitter as ElasticTransportStream & {
345
- destroy: (err?: Error) => void
346
- }
347
- splitterWithDestroy.flush = bulkSender.flush
348
- splitterWithDestroy.close = bulkSender.close
349
- const originalDestroy = splitterWithDestroy.destroy.bind(splitterWithDestroy)
350
- splitterWithDestroy.destroy = function (err?: Error) {
351
- void bulkSender.close()
352
- originalDestroy(err)
353
- }
354
-
355
- return splitterWithDestroy
356
- }
@@ -1,125 +0,0 @@
1
- /*
2
- Author : Mhmd Hammoud https://github.com/mhmdhammoud
3
- Date : 2023-06-17
4
- Description : String manipulation
5
- Version : 1.0
6
- */
7
- class Formatter {
8
- /**
9
- * @remarks Normalizes input by removing underscores and hyphens and capitalizing the first letter of the sentence.
10
- * @param stringToBeChanged - to be formatted without underscores and hyphens
11
- * @param withWhiteSpacing - Determines if white spacing is needed in the final string or not (default: true)
12
- * @returns string
13
- * @example
14
- * ```typescript
15
- * formatter.toUpperFirst('hello world')
16
- * // => 'Hello-World'
17
- * ```
18
- * */
19
- toUpperFirst = (str: string | number): string => {
20
- if (typeof str !== 'string') throw new Error('Provide a valid string')
21
-
22
- const formattedString = str
23
- .toString()
24
- .replace(/[^a-zA-Z0-9]+/g, '-')
25
- .trim()
26
- .toLowerCase()
27
-
28
- return formattedString.charAt(0).toUpperCase() + formattedString.slice(1)
29
- }
30
-
31
- /**
32
- * @remarks normalizes input to supported path and file name format.
33
- * Changes camelCase strings to kebab-case, replaces spaces with dash and keeps underscores. *
34
- * @param str - String needed to be modified
35
- * @returns formatted string
36
- */
37
- camelToKebab = (str: string | number): string => {
38
- if (typeof str !== 'string') {
39
- throw new Error('Invalid input. Please provide a valid string.')
40
- }
41
-
42
- const STRING_DASHERIZE_REGEXP = /\s/g
43
- const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g
44
-
45
- return str
46
- .replace(STRING_DECAMELIZE_REGEXP, '$1-$2')
47
- .toLowerCase()
48
- .replace(STRING_DASHERIZE_REGEXP, '-')
49
- }
50
-
51
- /**
52
- * @remarks obfuscates email address
53
- * @param email - email address to be obfuscated
54
- * @returns obfuscated email address
55
- * @example
56
- * ```typescript
57
- * formatter.obfuscate('johndoe@email.com') // => jo*****@gmail.com
58
- * ```
59
- * */
60
- obfuscate = (email: string) => {
61
- if (typeof email !== 'string') throw new Error('Provide a valid string')
62
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
63
- if (!emailRegex.test(email))
64
- throw new Error('Provide a valid email address')
65
- const separatorIndex = email.indexOf('@')
66
- if (separatorIndex < 3) {
67
- // 'ab@gmail.com' -> '**@gmail.com'
68
- return (
69
- email.slice(0, separatorIndex).replace(/./g, '*') +
70
- email.slice(separatorIndex)
71
- )
72
- }
73
- // 'test42@gmail.com' -> 'te****@gmail.com'
74
- return (
75
- email.slice(0, 2) +
76
- email.slice(2, separatorIndex).replace(/./g, '*') +
77
- email.slice(separatorIndex)
78
- )
79
- }
80
-
81
- /**
82
- * Capitalizes the first letter of each word in a sentence and removes non-alphanumeric characters.
83
- * @param sentence - The input sentence to be processed.
84
- * @returns A new sentence with the first letter of each word capitalized and non-alphanumeric characters removed.
85
- * @example
86
- * ```typescript
87
- * const sentence = 'this is34354345a---sentence'
88
- * const newSentence = ToUpperTitle(sentence)
89
- * console.log(newSentence) // 'This Is A Sentence'
90
- * ```
91
- */
92
- toUpperTitle = (sentence: string | number): string => {
93
- if (typeof sentence !== 'string') throw new Error('Provide a valid string')
94
- if (!sentence) throw new Error('Provide a valid string')
95
-
96
- const sanitizedSentence = sentence.replace(/[^a-zA-Z\s]+/g, ' ') // Exclude numbers from the character class
97
- const words = sanitizedSentence.split(/\s+/)
98
- const capitalizedWords = words.map((word) => {
99
- return word.charAt(0).toUpperCase() + word.slice(1)
100
- })
101
-
102
- return capitalizedWords.join(' ')
103
- }
104
-
105
- /**
106
- * @remarks Generates a slug from a given string
107
- * @param title - string to be converted to slug
108
- * @returns slug
109
- * @example
110
- * ```typescript
111
- * formatter.slugify('Hello World') // => hello-world
112
- * ```
113
- * */
114
- slugify = (title: string | number): string => {
115
- if (typeof title !== 'string') throw new Error('Provide a valid string')
116
-
117
- return title
118
- .replace(/[^\w\s]/g, '') // Remove non-alphanumeric characters
119
- .replace(/\s+/g, '-') // Replace consecutive spaces with a single dash
120
- .toLowerCase()
121
- }
122
- }
123
-
124
- const formatter = new Formatter()
125
- export default formatter
@@ -1,41 +0,0 @@
1
- import imagesLoaded from 'imagesloaded'
2
- /*
3
- Author : Mustafa Halabi https://github.com/mustafahalabi
4
- Date : 2024-01-29
5
- Description : Loads images and returns a promise
6
- Version : 1.0
7
- */
8
- class ImageFull {
9
- /**
10
- * @remarks
11
- * Receives either a className or an id or an element and returns a promise that resolves when all images are loaded
12
- * @param selector - either a className or an id or an element that contains images
13
- * @example
14
- * ```ts
15
- * .image-container
16
- * #image-container
17
- * img //as an element
18
- *
19
- * Imagefull.preloadImages('.image-container').then((event) => {
20
- * do something
21
- * })
22
- * ```
23
- *
24
- * @returns Promise<ImagesLoaded | undefined>
25
- *
26
- */
27
- preloadImages = (selector: string) => {
28
- return new Promise((resolve) => {
29
- imagesLoaded(
30
- document.querySelectorAll(selector),
31
- { background: true },
32
- (event) => {
33
- resolve(event)
34
- }
35
- )
36
- })
37
- }
38
- }
39
-
40
- const imagefull = new ImageFull()
41
- export default imagefull
package/src/lib/index.ts DELETED
@@ -1,12 +0,0 @@
1
- export { default as Crypto } from './cypto'
2
- export { default as Formatter } from './formatter'
3
- export { default as Pdf } from './formatter'
4
- export { default as Colorful } from './colorful'
5
- export { default as ImageFull } from './imagefull'
6
- export { default as Logger } from './logger'
7
- export {
8
- runWithTrace,
9
- runWithTraceSync,
10
- getTraceContext,
11
- type TraceContext,
12
- } from './trace-store'