dff-util 1.3.9 → 1.4.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 CHANGED
@@ -11,6 +11,7 @@ Select a topic below to view its reference and examples.
11
11
  | [AppRandomString](#general-utilities) | [AppUniqueCode](#general-utilities) | [AppCodeByType](#general-utilities) | [AppCode](#general-utilities) | [AppDaysBack](#general-utilities) | [AppAddDays](#general-utilities) | [AppUUID4](#general-utilities) |
12
12
  | [TruncateText](#text-and-date-formatting) | [DateTime24HrFormat](#text-and-date-formatting) | [DateTime12HrFormat](#text-and-date-formatting) | [DateAndTime](#text-and-date-formatting) | [TimeAgo](#text-and-date-formatting) | [toCamelCase](#case-conversion-and-object-mapping) | [toSnakeCase](#case-conversion-and-object-mapping) |
13
13
  | [toLowerCase](#case-conversion-and-object-mapping) | [toUpperCase](#case-conversion-and-object-mapping) | [toPascalCase](#case-conversion-and-object-mapping) | [toViewMapper](#case-conversion-and-object-mapping) | [toEntityMapper](#case-conversion-and-object-mapping) | [toSchemaMapper](#case-conversion-and-object-mapping) | [QueryCond](#query-conditions-querycond) |
14
+ | [CsvToJson](#csv-and-json-conversion) | [JsonToCsv](#csv-and-json-conversion) | | | | | |
14
15
 
15
16
  **[Http](#http)**
16
17
 
@@ -104,9 +105,9 @@ These examples reflect the current implementation. `toCamelCase` preserves inter
104
105
 
105
106
  | Function | Key conversion | Example input | Example output |
106
107
  | :--- | :--- | :--- | :--- |
107
- | `toViewMapper` | Convert keys to camel case and `_id` to `id` recursively | `{ _id: 'abc123', user_name: 'prasad', address_info: { city_name: 'hyderabad' } }` | `{ id: 'abc123', userName: 'prasad', addressInfo: { cityName: 'hyderabad' } }` |
108
- | `toEntityMapper` | Convert keys to snake case recursively; preserve `id` | `{ id: 'abc123', userName: 'prasad', addressInfo: { cityName: 'hyderabad' } }` | `{ id: 'abc123', user_name: 'prasad', address_info: { city_name: 'hyderabad' } }` |
109
- | `toSchemaMapper` | Convert top-level `id` to `_id` and keys to snake case; nested values use `toEntityMapper` | `{ id: 'abc123', userName: 'prasad', addressInfo: { cityName: 'hyderabad' } }` | `{ _id: 'abc123', user_name: 'prasad', address_info: { city_name: 'hyderabad' } }` |
108
+ | `toViewMapper` | Convert keys to camel case and `_id` to `id` recursively | `{ _id: 'abc123', user_name: 'k7m2x9', address_info: { city_name: 'hyderabad' } }` | `{ id: 'abc123', userName: 'k7m2x9', addressInfo: { cityName: 'hyderabad' } }` |
109
+ | `toEntityMapper` | Convert keys to snake case recursively; preserve `id` | `{ id: 'abc123', userName: 'k7m2x9', addressInfo: { cityName: 'hyderabad' } }` | `{ id: 'abc123', user_name: 'k7m2x9', address_info: { city_name: 'hyderabad' } }` |
110
+ | `toSchemaMapper` | Convert top-level `id` to `_id` and keys to snake case; nested values use `toEntityMapper` | `{ id: 'abc123', userName: 'k7m2x9', addressInfo: { cityName: 'hyderabad' } }` | `{ _id: 'abc123', user_name: 'k7m2x9', address_info: { city_name: 'hyderabad' } }` |
110
111
 
111
112
  All three preserve language maps whose keys are recognized language codes. Primitive and non-plain-object values are returned unchanged. `toViewMapper` and `toEntityMapper` process array items recursively; `toSchemaMapper` uses `toEntityMapper` for array items, so their `id` keys remain `id`.
112
113
 
@@ -300,6 +301,67 @@ QueryCond output: [
300
301
  ]
301
302
  ```
302
303
 
304
+ ### CSV and JSON conversion
305
+
306
+ `CsvToJson` and `JsonToCsv` convert between CSV text and JSON objects. The delimiter is optional and defaults to `,`. Fields that contain the delimiter, double quotes, or line breaks are quoted; quotes inside a field are escaped by doubling (`""`), following RFC 4180.
307
+
308
+ ```typescript
309
+ import { CsvToJson, JsonToCsv } from 'dff-util';
310
+
311
+ const rows = CsvToJson('name,note\n"Doe, John","He said ""hello"""');
312
+ // [{ name: 'Doe, John', note: 'He said "hello"' }]
313
+
314
+ const csv = JsonToCsv([
315
+ { name: 'Doe, John', note: 'He said "hello"' },
316
+ { name: 'k7m2x9', city: 'Hyderabad' },
317
+ ]);
318
+ // name,note,city
319
+ // "Doe, John","He said ""hello""",
320
+ // k7m2x9,,Hyderabad
321
+
322
+ const tsv = JsonToCsv([{ name: 'k7m2x9', city: 'Hyderabad' }], '\t');
323
+ const fromTsv = CsvToJson(tsv, '\t');
324
+
325
+ const multiline = CsvToJson(`name,bio
326
+ k7m2x9,"line1
327
+ line2"
328
+ n4t9pw,"a
329
+ b
330
+ c"`);
331
+ // [
332
+ // { name: 'k7m2x9', bio: 'line1\nline2' },
333
+ // { name: 'n4t9pw', bio: 'a\nb\nc' },
334
+ // ]
335
+
336
+ JsonToCsv([
337
+ { name: 'k7m2x9', note: 'line1\nline2' },
338
+ { name: 'n4t9pw', note: 'hello\r\nworld' },
339
+ ]);
340
+ // name,note
341
+ // k7m2x9,"line1
342
+ // line2"
343
+ // n4t9pw,"hello
344
+ // world"
345
+ ```
346
+
347
+ | Function | Arguments | Result |
348
+ | :--- | :--- | :--- |
349
+ | `CsvToJson` | `csv, delimiter = ','` | Array of objects; header row becomes keys |
350
+ | `JsonToCsv` | `data, delimiter = ','` | CSV text with a header row |
351
+
352
+ `JsonToCsv` accepts an object, an array of objects, or a JSON string of either. Nested objects and arrays are written with `JSON.stringify`. An empty array, `null`, or `undefined` input returns `''`. `CsvToJson` also accepts `null` or `undefined` and returns `[]`. Blank lines are skipped when they cannot be a data row. A leading BOM is stripped. `\n` and `\r\n` are accepted. Unterminated quoted fields throw.
353
+
354
+ Every header still becomes a property on each row. `null` and `undefined` write as empty cells; an empty cell parses as `null`.
355
+
356
+ | JSON value | CSV cell | Parsed value |
357
+ | :--- | :--- | :--- |
358
+ | `null` | empty cell | `null` |
359
+ | `undefined` | empty cell | `null` |
360
+ | missing key | empty cell | `null` |
361
+ | `''` | `""` | `''` |
362
+
363
+ The delimiter may be any non-empty string except a quote or line break, including `;`, `\t`, or `||`.
364
+
303
365
  ## Http
304
366
 
305
367
  ### HTTP requests
@@ -500,22 +562,20 @@ const fileBlob = await Http.Get(
500
562
 
501
563
  ## Call Api
502
564
 
503
- These helpers are defined in `src/main/api-call.ts` and exported from `dff-util`. All three use synchronous `XMLHttpRequest`: they return values directly and throw errors synchronously. No `await` is needed. Browser XHR support and server CORS permission are required; requests block while waiting.
565
+ These helpers are defined in `src/main/api-call.ts` and exported from `dff-util`. All three use `fetch` and return Promises. Use `await` (or `.then`) in an async function or a module that supports top-level await. A runtime with `fetch` is required (browsers and Node.js 18+). In the browser, the remote service must permit your origin through CORS.
504
566
 
505
567
  The former names `Hbs`, `LangText`, and `CurrencyConvert` are now `CallHbs`, `CallLangText`, and `CallCurrencyConvert`. Import them from `dff-util` or `main/api-call` instead of `main/util`.
506
568
 
507
569
  ### Handlebars rendering (CallHbs)
508
570
 
509
- `CallHbs` renders inline templates through `https://hbs.rndpro.in`, using the same request format as the HBS service. It uses synchronous `XMLHttpRequest` and sends the template and data to that service. It returns directly, without a Promise. Standard Node.js does not provide `XMLHttpRequest`.
510
-
511
- Synchronous requests block the browser while waiting and are deprecated on the main thread. The service must permit your browser origin and JSON POST requests through CORS. See [synchronous XHR limitations](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Synchronous_and_Asynchronous_Requests).
571
+ `CallHbs` renders inline templates through `https://hbs.rndpro.in`, using the same request format as the HBS service. It POSTs JSON `{ template, data }` with `Content-Type: application/json` and returns a Promise. The service must permit your origin and JSON POST requests through CORS.
512
572
 
513
573
  | Call | Endpoint | Return type |
514
574
  | :--- | :--- | :--- |
515
- | `CallHbs(template, data)` | `POST /render` | `string` |
516
- | `CallHbs(template, items, true)` | `POST /multi` | `string[]` |
575
+ | `await CallHbs(template, data)` | `POST /render` | `Promise<string>` |
576
+ | `await CallHbs(template, items, true)` | `POST /multi` | `Promise<string[]>` |
517
577
 
518
- The TypeScript return type is `string | string[]`; the table shows the result for each mode.
578
+ The TypeScript return type is `Promise<string | string[]>`; the table shows the result for each mode.
519
579
 
520
580
  #### Supported inputs and rendering modes
521
581
 
@@ -524,10 +584,10 @@ Based on the `dff-hbs` service in the local `hbs` repository (`src/models.rs`, `
524
584
  | Input or mode | Type | Behavior / example |
525
585
  | :--- | :--- | :--- |
526
586
  | `template` | `string` | Inline Handlebars text, such as `Hello, {{name}}!`; not a registered template name |
527
- | `data` with `multi = false` (default) | `RequestBodyType` | One object used as the template context: `{ name: 'Prasad' }` |
587
+ | `data` with `multi = false` (default) | `RequestBodyType` | One object used as the template context: `{ name: 'k7m2x9' }` |
528
588
  | `data` with `multi = false` | `RequestBodyType[]` | One array context; iterate using `{{#each this}}{{name}}{{/each}}` |
529
589
  | `data` with `multi = true` | `RequestBodyType[]` | Render the same template for each object in one `/multi` request |
530
- | Object property values | JSON string, number, boolean, null, object, or array | Nested data is supported, for example `{ user: { name: 'Prasad' }, active: true }` |
590
+ | Object property values | JSON string, number, boolean, null, object, or array | Nested data is supported, for example `{ user: { name: 'k7m2x9' }, active: true }` |
531
591
  | Single result | `string` | Rendered text from `/render` |
532
592
  | Batch result | `string[]` | Rendered strings in input order from `/multi`; an empty array returns `[]` |
533
593
  | `data` is `null`/`undefined` | no request | Returns `template` unchanged; `[template]` when `multi = true` |
@@ -575,21 +635,21 @@ Quote examples show the helper value before any HTML escaping. The registered he
575
635
  ```typescript
576
636
  import { CallHbs } from 'dff-util';
577
637
 
578
- const rendered = CallHbs('Hello, {{to_pascal_case name}}!', {
579
- name: 'prasad',
638
+ const rendered = await CallHbs('Hello, {{to_pascal_case name}}!', {
639
+ name: 'k7m2x9',
580
640
  });
581
- console.log(rendered); // Hello, Prasad!
641
+ console.log(rendered); // Hello, K7m2x9!
582
642
  ```
583
643
 
584
644
  #### Render multiple data items in one request
585
645
 
586
646
  ```typescript
587
- const rendered = CallHbs(
647
+ const rendered = await CallHbs(
588
648
  'Hello, {{to_pascal_case name}}!',
589
- [{ name: 'prasad' }, { name: 'monika' }],
649
+ [{ name: 'k7m2x9' }, { name: 'n4t9pw' }],
590
650
  true,
591
651
  );
592
- console.log(rendered); // ['Hello, Prasad!', 'Hello, Monika!']
652
+ console.log(rendered); // ['Hello, K7m2x9!', 'Hello, N4t9pw!']
593
653
  ```
594
654
 
595
655
  The third argument defaults to `false`. Set it to `true` to render the same template once per array item, preserving order. An empty batch returns `[]`. With `false`, an array is treated as a single template context, for example with `{{#each this}}`.
@@ -600,49 +660,49 @@ Both endpoints receive JSON in the form `{ template, data }`. Templates can use
600
660
 
601
661
  ```typescript
602
662
  try {
603
- const result = CallHbs('Hello, {{name}}!', { name: 'Prasad' });
663
+ const result = await CallHbs('Hello, {{name}}!', { name: 'k7m2x9' });
604
664
  console.log(result);
605
665
  } catch (error) {
606
666
  console.error(error instanceof Error ? error.message : error);
607
667
  }
608
668
  ```
609
669
 
610
- Non-success HTTP responses throw an error containing the status and response text. The service returns `400` for invalid requests and `500` for rendering failures; a failing batch reports the item index and rejects the entire batch. Network, CORS, and JSON-parsing errors propagate to the caller. Successful batch responses are checked to ensure they contain an array of strings.
670
+ Non-success HTTP responses reject with an error containing the status and response text. The service returns `400` for invalid requests and `500` for rendering failures; a failing batch reports the item index and rejects the entire batch. Network, CORS, and JSON-parsing errors propagate to the caller. Successful batch responses are checked to ensure they contain an array of strings.
611
671
 
612
672
  ### Language translation (CallLangText)
613
673
 
614
- `CallLangText(text, sourceLocale, targetLocale)` returns the translated text as a `string`.
674
+ `CallLangText(text, sourceLocale, targetLocale)` returns a Promise that resolves to the translated text as a `string`.
615
675
 
616
676
  ```typescript
617
677
  import { CallLangText } from 'dff-util';
618
678
 
619
679
  try {
620
- const translated = CallLangText('Hello, how are you?', 'en-US', 'te-IN');
680
+ const translated = await CallLangText('Hello, how are you?', 'en-US', 'te-IN');
621
681
  console.log(translated);
622
682
  } catch (error) {
623
683
  console.error(error instanceof Error ? error.message : error);
624
684
  }
625
685
  ```
626
686
 
627
- Uses `https://lingva.ml/api/v1`, taking the two-letter language prefix from each locale. Text is URL-encoded. Empty inputs, invalid language codes, unsuccessful HTTP responses, and service-reported errors throw.
687
+ Uses `https://lingva.ml/api/v1`, taking the two-letter language prefix from each locale. Text is URL-encoded. Empty inputs, invalid language codes, unsuccessful HTTP responses, and service-reported errors reject.
628
688
 
629
689
  ### Currency conversion (CallCurrencyConvert)
630
690
 
631
- `CallCurrencyConvert(fromCurrency, toCurrency)` returns the exchange rate as a `number`: the target-currency value of one source-currency unit.
691
+ `CallCurrencyConvert(fromCurrency, toCurrency)` returns a Promise that resolves to the exchange rate as a `number`: the target-currency value of one source-currency unit.
632
692
 
633
693
  ```typescript
634
694
  import { CallCurrencyConvert } from 'dff-util';
635
695
 
636
696
  try {
637
- const rate = CallCurrencyConvert('INR', 'USD');
697
+ const rate = await CallCurrencyConvert('INR', 'USD');
638
698
  console.log(rate);
639
- console.log(CallCurrencyConvert('USD', 'USD')); // 1; no HTTP request
699
+ console.log(await CallCurrencyConvert('USD', 'USD')); // 1; no HTTP request
640
700
  } catch (error) {
641
701
  console.error(error instanceof Error ? error.message : error);
642
702
  }
643
703
  ```
644
704
 
645
- Uses `https://api.frankfurter.dev/v1/latest`. Missing inputs, unsuccessful HTTP responses, and missing or nonnumeric rates throw. Network and JSON-parsing errors propagate to the caller.
705
+ Uses `https://api.frankfurter.dev/v1/latest`. Missing inputs, unsuccessful HTTP responses, and missing or nonnumeric rates reject. Network and JSON-parsing errors propagate to the caller.
646
706
 
647
707
  ## Tokens and encryption
648
708
 
@@ -1,6 +1,6 @@
1
1
  import { RequestBodyType } from './const-type';
2
2
  /** Render an inline Handlebars template using the hosted HBS service. */
3
- export declare function CallHbs(template: string, data: RequestBodyType | RequestBodyType[] | null | undefined, multi?: boolean): string | string[];
4
- export declare const CallLangText: (data: string, source: string, target: string) => string;
3
+ export declare function CallHbs(template: string, data: RequestBodyType | RequestBodyType[] | null | undefined, multi?: boolean): Promise<string | string[]>;
4
+ export declare const CallLangText: (data: string, source: string, target: string) => Promise<string>;
5
5
  /** Return the exchange rate for one unit of the source currency. */
6
- export declare const CallCurrencyConvert: (from: string, to: string) => number;
6
+ export declare const CallCurrencyConvert: (from: string, to: string) => Promise<number>;
@@ -1,50 +1,65 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
12
  exports.CallCurrencyConvert = exports.CallLangText = void 0;
4
13
  exports.CallHbs = CallHbs;
5
- // All API calls are synchronous and require XMLHttpRequest support.
6
14
  function requestText(url, label, body) {
7
- if (typeof XMLHttpRequest === 'undefined') {
8
- throw new Error(`${label} requires a browser with synchronous XMLHttpRequest support`);
9
- }
10
- const request = new XMLHttpRequest();
11
- request.open(body === undefined ? 'GET' : 'POST', url, false);
12
- if (body !== undefined)
13
- request.setRequestHeader('Content-Type', 'application/json');
14
- request.send(body === undefined ? null : JSON.stringify(body));
15
- if (request.status < 200 || request.status >= 300) {
16
- const message = request.responseText;
17
- throw new Error(`${label} failed: ${request.status} ${request.statusText}${message ? `: ${message}` : ''}`);
18
- }
19
- return request.responseText;
15
+ return __awaiter(this, void 0, void 0, function* () {
16
+ if (typeof fetch === 'undefined') {
17
+ throw new Error(`${label} requires a runtime with fetch support`);
18
+ }
19
+ const init = body === undefined
20
+ ? { method: 'GET' }
21
+ : {
22
+ method: 'POST',
23
+ headers: { 'Content-Type': 'application/json' },
24
+ body: JSON.stringify(body),
25
+ };
26
+ const response = yield fetch(url, init);
27
+ const text = yield response.text();
28
+ if (!response.ok) {
29
+ throw new Error(`${label} failed: ${response.status} ${response.statusText}${text ? `: ${text}` : ''}`);
30
+ }
31
+ return text;
32
+ });
20
33
  }
21
34
  /** Render an inline Handlebars template using the hosted HBS service. */
22
- function CallHbs(template, data, multi = false) {
23
- if (typeof template !== 'string') {
24
- throw new Error('Invalid input: template must be a string');
25
- }
26
- // With no data, no request is made: return the template unchanged,
27
- // or a single-item array when rendering in multi mode.
28
- if (data === null || data === undefined) {
29
- return multi ? [template] : template;
30
- }
31
- const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
32
- if (!(Array.isArray(data) ? data.every(isObject) : isObject(data))) {
33
- throw new Error('Invalid input: data must be a JSON object or an array of JSON objects');
34
- }
35
- if (multi && !Array.isArray(data)) {
36
- throw new Error('Invalid input: data must be an array for multi rendering');
37
- }
38
- const text = requestText(`https://hbs.rndpro.in/${multi ? 'multi' : 'render'}`, 'HBS rendering', { template, data });
39
- if (!multi)
40
- return text;
41
- const rendered = JSON.parse(text);
42
- if (!Array.isArray(rendered) || !rendered.every((item) => typeof item === 'string')) {
43
- throw new Error('Invalid HBS response: expected an array of strings');
44
- }
45
- return rendered;
35
+ function CallHbs(template_1, data_1) {
36
+ return __awaiter(this, arguments, void 0, function* (template, data, multi = false) {
37
+ if (typeof template !== 'string') {
38
+ throw new Error('Invalid input: template must be a string');
39
+ }
40
+ // With no data, no request is made: return the template unchanged,
41
+ // or a single-item array when rendering in multi mode.
42
+ if (data === null || data === undefined) {
43
+ return multi ? [template] : template;
44
+ }
45
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
46
+ if (!(Array.isArray(data) ? data.every(isObject) : isObject(data))) {
47
+ throw new Error('Invalid input: data must be a JSON object or an array of JSON objects');
48
+ }
49
+ if (multi && !Array.isArray(data)) {
50
+ throw new Error('Invalid input: data must be an array for multi rendering');
51
+ }
52
+ const text = yield requestText(`https://hbs.rndpro.in/${multi ? 'multi' : 'render'}`, 'HBS rendering', { template, data });
53
+ if (!multi)
54
+ return text;
55
+ const rendered = JSON.parse(text);
56
+ if (!Array.isArray(rendered) || !rendered.every((item) => typeof item === 'string')) {
57
+ throw new Error('Invalid HBS response: expected an array of strings');
58
+ }
59
+ return rendered;
60
+ });
46
61
  }
47
- const CallLangText = (data, source, target) => {
62
+ const CallLangText = (data, source, target) => __awaiter(void 0, void 0, void 0, function* () {
48
63
  if (!data || !source || !target) {
49
64
  throw new Error('Invalid input: data, source, or target is missing');
50
65
  }
@@ -55,7 +70,7 @@ const CallLangText = (data, source, target) => {
55
70
  }
56
71
  const encodedData = encodeURIComponent(data);
57
72
  const url = `https://lingva.ml/api/v1/${src}/${tgt}/${encodedData}`;
58
- const json = JSON.parse(requestText(url, 'Translation'));
73
+ const json = JSON.parse(yield requestText(url, 'Translation'));
59
74
  if (json.error) {
60
75
  throw new Error(`API error: ${json.error}`);
61
76
  }
@@ -63,10 +78,10 @@ const CallLangText = (data, source, target) => {
63
78
  throw new Error('Invalid translation response: expected a string');
64
79
  }
65
80
  return json.translation || '';
66
- };
81
+ });
67
82
  exports.CallLangText = CallLangText;
68
83
  /** Return the exchange rate for one unit of the source currency. */
69
- const CallCurrencyConvert = (from, to) => {
84
+ const CallCurrencyConvert = (from, to) => __awaiter(void 0, void 0, void 0, function* () {
70
85
  var _a;
71
86
  if (!from || !to) {
72
87
  throw new Error('Invalid input: from or to is missing');
@@ -74,11 +89,11 @@ const CallCurrencyConvert = (from, to) => {
74
89
  if (from === to)
75
90
  return 1;
76
91
  const url = `https://api.frankfurter.dev/v1/latest?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`;
77
- const json = JSON.parse(requestText(url, 'Currency conversion'));
92
+ const json = JSON.parse(yield requestText(url, 'Currency conversion'));
78
93
  const rate = (_a = json.rates) === null || _a === void 0 ? void 0 : _a[to];
79
94
  if (typeof rate !== 'number' || !Number.isFinite(rate)) {
80
95
  throw new Error('Invalid currency response: expected a numeric exchange rate');
81
96
  }
82
97
  return rate;
83
- };
98
+ });
84
99
  exports.CallCurrencyConvert = CallCurrencyConvert;
@@ -0,0 +1,5 @@
1
+ export type CsvCell = string | null;
2
+ /** Parse CSV text into an array of objects. The first row is used as header keys. */
3
+ export declare function CsvToJson(csv: string | null | undefined, delimiter?: string): Record<string, CsvCell>[];
4
+ /** Serialize JSON objects as CSV text. Nested objects and arrays are JSON-stringified. */
5
+ export declare function JsonToCsv(data: string | Record<string, unknown> | Record<string, unknown>[] | null | undefined, delimiter?: string): string;
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CsvToJson = CsvToJson;
4
+ exports.JsonToCsv = JsonToCsv;
5
+ function normalizeDelimiter(delimiter) {
6
+ if (typeof delimiter !== 'string' || delimiter.length === 0) {
7
+ throw new Error('Invalid input: delimiter must be a non-empty string');
8
+ }
9
+ if (delimiter.includes('"') || delimiter.includes('\n') || delimiter.includes('\r')) {
10
+ throw new Error('Invalid input: delimiter cannot contain quotes or line breaks');
11
+ }
12
+ return delimiter;
13
+ }
14
+ function isPlainObject(value) {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
16
+ }
17
+ function needsQuotes(value, delimiter) {
18
+ return (value === '' ||
19
+ value.includes('"') ||
20
+ value.includes('\n') ||
21
+ value.includes('\r') ||
22
+ value.includes(delimiter));
23
+ }
24
+ function encodeField(value, delimiter) {
25
+ if (needsQuotes(value, delimiter)) {
26
+ return `"${value.replace(/"/g, '""')}"`;
27
+ }
28
+ return value;
29
+ }
30
+ function encodeValue(value, delimiter) {
31
+ if (value === undefined || value === null)
32
+ return '';
33
+ if (typeof value === 'string')
34
+ return encodeField(value, delimiter);
35
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
36
+ return String(value);
37
+ }
38
+ if (value instanceof Date)
39
+ return encodeField(value.toISOString(), delimiter);
40
+ if (typeof value === 'object')
41
+ return encodeField(JSON.stringify(value), delimiter);
42
+ return encodeField(String(value), delimiter);
43
+ }
44
+ function decodeCell(field) {
45
+ if (!field)
46
+ return null;
47
+ if (field.quoted)
48
+ return field.value;
49
+ if (field.value === '')
50
+ return null;
51
+ return field.value;
52
+ }
53
+ function parseCsvRows(input, delimiter) {
54
+ if (input.charCodeAt(0) === 0xfeff)
55
+ input = input.slice(1);
56
+ if (input.length === 0)
57
+ return [];
58
+ const rows = [];
59
+ let row = [];
60
+ let field = '';
61
+ let quoted = false;
62
+ let inQuotes = false;
63
+ const dLen = delimiter.length;
64
+ const pushField = () => {
65
+ row.push({ value: field, quoted });
66
+ field = '';
67
+ quoted = false;
68
+ };
69
+ for (let i = 0; i < input.length; i++) {
70
+ const ch = input[i];
71
+ if (inQuotes) {
72
+ if (ch === '"') {
73
+ if (input[i + 1] === '"') {
74
+ field += '"';
75
+ i++;
76
+ }
77
+ else {
78
+ inQuotes = false;
79
+ }
80
+ }
81
+ else {
82
+ field += ch;
83
+ }
84
+ continue;
85
+ }
86
+ if (ch === '"' && field === '') {
87
+ inQuotes = true;
88
+ quoted = true;
89
+ continue;
90
+ }
91
+ if (input.startsWith(delimiter, i)) {
92
+ pushField();
93
+ i += dLen - 1;
94
+ continue;
95
+ }
96
+ if (ch === '\n' || ch === '\r') {
97
+ pushField();
98
+ rows.push(row);
99
+ row = [];
100
+ if (ch === '\r' && input[i + 1] === '\n')
101
+ i++;
102
+ continue;
103
+ }
104
+ field += ch;
105
+ }
106
+ if (inQuotes) {
107
+ throw new Error('Invalid CSV: unterminated quoted field');
108
+ }
109
+ if (!(input.endsWith('\n') || input.endsWith('\r'))) {
110
+ pushField();
111
+ rows.push(row);
112
+ }
113
+ return rows;
114
+ }
115
+ function isBlankLine(row, columnCount) {
116
+ return row.length === 1 && row[0].value === '' && !row[0].quoted && columnCount > 1;
117
+ }
118
+ function isEmptyRow(row) {
119
+ return row.every((cell) => cell.value === '' && !cell.quoted);
120
+ }
121
+ /** Parse CSV text into an array of objects. The first row is used as header keys. */
122
+ function CsvToJson(csv, delimiter = ',') {
123
+ if (csv === null || csv === undefined)
124
+ return [];
125
+ if (typeof csv !== 'string') {
126
+ throw new Error('Invalid input: csv must be a string');
127
+ }
128
+ const rows = parseCsvRows(csv, normalizeDelimiter(delimiter));
129
+ if (rows.length === 0 || (rows.length === 1 && isEmptyRow(rows[0])))
130
+ return [];
131
+ const headers = rows[0].map((cell) => cell.value);
132
+ const records = [];
133
+ for (let r = 1; r < rows.length; r++) {
134
+ const row = rows[r];
135
+ if (isBlankLine(row, headers.length))
136
+ continue;
137
+ const record = {};
138
+ for (let c = 0; c < headers.length; c++) {
139
+ const key = headers[c];
140
+ if (key === '')
141
+ continue;
142
+ record[key] = decodeCell(row[c]);
143
+ }
144
+ records.push(record);
145
+ }
146
+ return records;
147
+ }
148
+ /** Serialize JSON objects as CSV text. Nested objects and arrays are JSON-stringified. */
149
+ function JsonToCsv(data, delimiter = ',') {
150
+ const delim = normalizeDelimiter(delimiter);
151
+ let records = data;
152
+ if (typeof records === 'string') {
153
+ if (records.trim() === '')
154
+ return '';
155
+ try {
156
+ records = JSON.parse(records);
157
+ }
158
+ catch (_a) {
159
+ throw new Error('Invalid input: data must be valid JSON');
160
+ }
161
+ }
162
+ if (records === null || records === undefined)
163
+ return '';
164
+ if (!Array.isArray(records)) {
165
+ if (!isPlainObject(records)) {
166
+ throw new Error('Invalid input: data must be an object or an array of objects');
167
+ }
168
+ records = [records];
169
+ }
170
+ const rows = records;
171
+ if (rows.length === 0)
172
+ return '';
173
+ if (!rows.every((row) => row === null || row === undefined || isPlainObject(row))) {
174
+ throw new Error('Invalid input: data must be an object or an array of objects');
175
+ }
176
+ const keys = [];
177
+ const seen = new Set();
178
+ for (const record of rows) {
179
+ if (!isPlainObject(record))
180
+ continue;
181
+ for (const key of Object.keys(record)) {
182
+ if (!seen.has(key)) {
183
+ seen.add(key);
184
+ keys.push(key);
185
+ }
186
+ }
187
+ }
188
+ if (keys.length === 0)
189
+ return '';
190
+ const lines = [keys.map((key) => encodeField(key, delim)).join(delim)];
191
+ for (const record of rows) {
192
+ if (record === null || record === undefined) {
193
+ lines.push(keys.map(() => '').join(delim));
194
+ continue;
195
+ }
196
+ const obj = record;
197
+ lines.push(keys.map((key) => encodeValue(obj[key], delim)).join(delim));
198
+ }
199
+ return lines.join('\n');
200
+ }
@@ -19,3 +19,4 @@ export declare function DecodeURL(input: string): string;
19
19
  export declare function SafeEncode(input: string): string;
20
20
  export declare function SafeDecode(encoded: string): string;
21
21
  export { QueryCond, type QueryCondItem } from './query-cond';
22
+ export { CsvToJson, JsonToCsv, type CsvCell } from './csv';
package/dist/main/util.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.QueryCond = exports.LangCountryCode = exports.TimeAgo = exports.DateAndTime = exports.DateTime12HrFormat = exports.DateTime24HrFormat = exports.TruncateText = exports.AppUUID4 = exports.AppAddDays = exports.AppDaysBack = exports.AppCode = exports.AppCodeByType = exports.AppUniqueCode = exports.AppRandomString = void 0;
3
+ exports.JsonToCsv = exports.CsvToJson = exports.QueryCond = exports.LangCountryCode = exports.TimeAgo = exports.DateAndTime = exports.DateTime12HrFormat = exports.DateTime24HrFormat = exports.TruncateText = exports.AppUUID4 = exports.AppAddDays = exports.AppDaysBack = exports.AppCode = exports.AppCodeByType = exports.AppUniqueCode = exports.AppRandomString = void 0;
4
4
  exports.EncodeBase64 = EncodeBase64;
5
5
  exports.DecodeBase64 = DecodeBase64;
6
6
  exports.EncodeURL = EncodeURL;
@@ -197,3 +197,6 @@ function SafeDecode(encoded) {
197
197
  }
198
198
  var query_cond_1 = require("./query-cond");
199
199
  Object.defineProperty(exports, "QueryCond", { enumerable: true, get: function () { return query_cond_1.QueryCond; } });
200
+ var csv_1 = require("./csv");
201
+ Object.defineProperty(exports, "CsvToJson", { enumerable: true, get: function () { return csv_1.CsvToJson; } });
202
+ Object.defineProperty(exports, "JsonToCsv", { enumerable: true, get: function () { return csv_1.JsonToCsv; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dff-util",
3
- "version": "1.3.9",
3
+ "version": "1.4.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {