csv-to-pg 0.6.2 → 2.0.2

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2020 Dan Lynch <pyramation@gmail.com>
3
+ Copyright (c) 2024 Dan Lynch <pyramation@gmail.com>
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/cli.js ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ // @ts-nocheck
5
+ const prompt_1 = require("@pyramation/prompt");
6
+ const parse_1 = require("./parse");
7
+ const parser_1 = require("./parser");
8
+ const utils_1 = require("./utils");
9
+ const path_1 = require("path");
10
+ const fs_1 = require("fs");
11
+ const argv = process.argv.slice(2);
12
+ (async () => {
13
+ let { config } = await (0, prompt_1.prompt)([
14
+ {
15
+ _: true,
16
+ name: 'config',
17
+ type: 'config',
18
+ required: true
19
+ }
20
+ ], argv);
21
+ config = (0, utils_1.normalizePath)(config);
22
+ const dir = (0, path_1.dirname)(config);
23
+ config = (0, parse_1.readConfig)(config);
24
+ if (config.input) {
25
+ if (!argv.includes('--input')) {
26
+ argv.push('--input');
27
+ config.input = (0, utils_1.normalizePath)(config.input, dir);
28
+ argv.push(config.input);
29
+ }
30
+ }
31
+ if (config.output) {
32
+ if (!argv.includes('--output')) {
33
+ argv.push('--output');
34
+ config.output = (0, utils_1.normalizePath)(config.output, dir);
35
+ argv.push(config.output);
36
+ }
37
+ }
38
+ const results = await (0, prompt_1.prompt)([
39
+ {
40
+ name: 'input',
41
+ type: 'path',
42
+ required: true
43
+ },
44
+ {
45
+ name: 'output',
46
+ type: 'path',
47
+ required: true
48
+ }
49
+ ], argv);
50
+ config.input = results.input;
51
+ let outFile = results.output;
52
+ if (!outFile.endsWith('.sql'))
53
+ outFile = outFile + '.sql';
54
+ if (argv.includes('--debug')) {
55
+ config.debug = true;
56
+ }
57
+ const parser = new parser_1.Parser(config);
58
+ const sql = await parser.parse();
59
+ (0, fs_1.writeFileSync)(config.output, sql);
60
+ })();
package/esm/cli.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // @ts-nocheck
3
+ import { prompt } from '@pyramation/prompt';
4
+ import { readConfig } from './parse';
5
+ import { Parser } from './parser';
6
+ import { normalizePath } from './utils';
7
+ import { dirname } from 'path';
8
+ import { writeFileSync } from 'fs';
9
+ const argv = process.argv.slice(2);
10
+ (async () => {
11
+ let { config } = await prompt([
12
+ {
13
+ _: true,
14
+ name: 'config',
15
+ type: 'config',
16
+ required: true
17
+ }
18
+ ], argv);
19
+ config = normalizePath(config);
20
+ const dir = dirname(config);
21
+ config = readConfig(config);
22
+ if (config.input) {
23
+ if (!argv.includes('--input')) {
24
+ argv.push('--input');
25
+ config.input = normalizePath(config.input, dir);
26
+ argv.push(config.input);
27
+ }
28
+ }
29
+ if (config.output) {
30
+ if (!argv.includes('--output')) {
31
+ argv.push('--output');
32
+ config.output = normalizePath(config.output, dir);
33
+ argv.push(config.output);
34
+ }
35
+ }
36
+ const results = await prompt([
37
+ {
38
+ name: 'input',
39
+ type: 'path',
40
+ required: true
41
+ },
42
+ {
43
+ name: 'output',
44
+ type: 'path',
45
+ required: true
46
+ }
47
+ ], argv);
48
+ config.input = results.input;
49
+ let outFile = results.output;
50
+ if (!outFile.endsWith('.sql'))
51
+ outFile = outFile + '.sql';
52
+ if (argv.includes('--debug')) {
53
+ config.debug = true;
54
+ }
55
+ const parser = new Parser(config);
56
+ const sql = await parser.parse();
57
+ writeFileSync(config.output, sql);
58
+ })();
package/esm/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // @ts-nocheck
2
+ export * from './parse';
3
+ export * from './parser';
package/esm/parse.js ADDED
@@ -0,0 +1,257 @@
1
+ // @ts-nocheck
2
+ import csv from 'csv-parser';
3
+ import { createReadStream, readFileSync } from 'fs';
4
+ import { safeLoad as parseYAML } from 'js-yaml';
5
+ import * as ast from 'pg-ast';
6
+ import { makeBoundingBox, makeLocation, getRelatedField, wrapValue } from './utils';
7
+ function isNumeric(str) {
8
+ if (typeof str === 'number')
9
+ return true;
10
+ if (typeof str !== 'string')
11
+ return false; // we only process strings!
12
+ return (!isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
13
+ !isNaN(parseFloat(str))); // ...and ensure strings of whitespace fail
14
+ }
15
+ const parseJson = (value) => {
16
+ if (typeof value === 'string')
17
+ return value;
18
+ return value && JSON.stringify(value);
19
+ };
20
+ const psqlArray = (value) => {
21
+ if (value && value.length) {
22
+ return `{${value.map((v) => v)}}`;
23
+ }
24
+ };
25
+ export const parse = (path, opts) => new Promise((resolve, reject) => {
26
+ const results = [];
27
+ createReadStream(path)
28
+ .pipe(csv(opts))
29
+ // TODO check if 'data' is guaranteed to have a full row,
30
+ // if so, make a hook to use the stream properly
31
+ .on('data', (data) => results.push(data))
32
+ .on('error', (er) => {
33
+ reject(er);
34
+ })
35
+ .on('end', () => {
36
+ resolve(results);
37
+ });
38
+ });
39
+ export const readConfig = (config) => {
40
+ let configValue;
41
+ if (config.endsWith('.js')) {
42
+ configValue = require(config);
43
+ }
44
+ else if (config.endsWith('json')) {
45
+ configValue = JSON.parse(readFileSync(config, 'utf-8'));
46
+ }
47
+ else if (config.endsWith('yaml') || config.endsWith('yml')) {
48
+ configValue = parseYAML(readFileSync(config, 'utf-8'));
49
+ }
50
+ else {
51
+ throw new Error('unsupported config!');
52
+ }
53
+ return configValue;
54
+ };
55
+ const getFromValue = (from) => {
56
+ if (Array.isArray(from))
57
+ return from;
58
+ return [from];
59
+ };
60
+ // looks like the CSV library gives us empty strings?
61
+ const cleanseEmptyStrings = (str) => {
62
+ if (typeof str === 'string') {
63
+ if (str.trim() === '')
64
+ return null;
65
+ return str;
66
+ }
67
+ else {
68
+ return str;
69
+ }
70
+ };
71
+ const parseBoolean = (str) => {
72
+ if (typeof str === 'boolean') {
73
+ return str;
74
+ }
75
+ else if (typeof str === 'string') {
76
+ const s = str.toLowerCase();
77
+ if (s === 'true') {
78
+ return true;
79
+ }
80
+ else if (s === 't') {
81
+ return true;
82
+ }
83
+ else if (s === 'f') {
84
+ return false;
85
+ }
86
+ else if (s === 'false') {
87
+ return false;
88
+ }
89
+ return null;
90
+ }
91
+ else {
92
+ return null;
93
+ }
94
+ };
95
+ const getValuesFromKeys = (object, keys) => keys.map((key) => object[key]);
96
+ const identity = (a) => a;
97
+ const isEmpty = (value) => value === null || typeof value === 'undefined';
98
+ // type (int, text, etc)
99
+ // from Array of keys that map to records found (e.g., ['lon', 'lat'])
100
+ const getCoercionFunc = (type, from, opts) => {
101
+ const parse = (opts.parse = opts.parse || identity);
102
+ switch (type) {
103
+ case 'int':
104
+ return (record) => {
105
+ const value = parse(record[from[0]]);
106
+ if (isEmpty(value)) {
107
+ return ast.Null({});
108
+ }
109
+ if (!isNumeric(value)) {
110
+ return ast.Null({});
111
+ }
112
+ const val = ast.A_Const({
113
+ val: ast.Integer({ ival: value })
114
+ });
115
+ return wrapValue(val, opts);
116
+ };
117
+ case 'float':
118
+ return (record) => {
119
+ const value = parse(record[from[0]]);
120
+ if (isEmpty(value)) {
121
+ return ast.Null({});
122
+ }
123
+ if (!isNumeric(value)) {
124
+ return ast.Null({});
125
+ }
126
+ const val = ast.A_Const({
127
+ val: ast.Float({ str: value })
128
+ });
129
+ return wrapValue(val, opts);
130
+ };
131
+ case 'boolean':
132
+ case 'bool':
133
+ return (record) => {
134
+ const value = parse(parseBoolean(record[from[0]]));
135
+ if (isEmpty(value)) {
136
+ return ast.Null({});
137
+ }
138
+ const val = ast.String({
139
+ str: value ? 'TRUE' : 'FALSE'
140
+ });
141
+ return wrapValue(val, opts);
142
+ };
143
+ case 'bbox':
144
+ // do bbox magic with args from the fields
145
+ return (record) => {
146
+ const val = makeBoundingBox(parse(record[from[0]]));
147
+ return wrapValue(val, opts);
148
+ };
149
+ case 'location':
150
+ return (record) => {
151
+ const [lon, lat] = getValuesFromKeys(record, from);
152
+ if (typeof lon === 'undefined') {
153
+ return ast.Null({});
154
+ }
155
+ if (typeof lat === 'undefined') {
156
+ return ast.Null({});
157
+ }
158
+ if (!isNumeric(lon) || !isNumeric(lat)) {
159
+ return ast.Null({});
160
+ }
161
+ // NO parse here...
162
+ const val = makeLocation(lon, lat);
163
+ return wrapValue(val, opts);
164
+ };
165
+ case 'related':
166
+ return (record) => {
167
+ return getRelatedField({
168
+ ...opts,
169
+ record,
170
+ from
171
+ });
172
+ };
173
+ case 'uuid':
174
+ return (record) => {
175
+ const value = parse(record[from[0]]);
176
+ if (isEmpty(value) ||
177
+ !/^([0-9a-fA-F]{8})-(([0-9a-fA-F]{4}-){3})([0-9a-fA-F]{12})$/i.test(value)) {
178
+ return ast.Null({});
179
+ }
180
+ const val = ast.A_Const({
181
+ val: ast.String({ str: value })
182
+ });
183
+ return wrapValue(val, opts);
184
+ };
185
+ case 'text':
186
+ return (record) => {
187
+ const value = parse(cleanseEmptyStrings(record[from[0]]));
188
+ if (isEmpty(value)) {
189
+ return ast.Null({});
190
+ }
191
+ const val = ast.A_Const({
192
+ val: ast.String({ str: value })
193
+ });
194
+ return wrapValue(val, opts);
195
+ };
196
+ case 'text[]':
197
+ return (record) => {
198
+ const value = parse(psqlArray(cleanseEmptyStrings(record[from[0]])));
199
+ if (isEmpty(value)) {
200
+ return ast.Null({});
201
+ }
202
+ const val = ast.A_Const({
203
+ val: ast.String({ str: value })
204
+ });
205
+ return wrapValue(val, opts);
206
+ };
207
+ case 'image':
208
+ case 'attachment':
209
+ case 'json':
210
+ case 'jsonb':
211
+ return (record) => {
212
+ const value = parse(parseJson(cleanseEmptyStrings(record[from[0]])));
213
+ if (isEmpty(value)) {
214
+ return ast.Null({});
215
+ }
216
+ const val = ast.A_Const({
217
+ val: ast.String({ str: value })
218
+ });
219
+ return wrapValue(val, opts);
220
+ };
221
+ default:
222
+ return (record) => {
223
+ const value = parse(cleanseEmptyStrings(record[from[0]]));
224
+ if (isEmpty(value)) {
225
+ return ast.Null({});
226
+ }
227
+ const val = ast.A_Const({
228
+ val: ast.String({ str: value })
229
+ });
230
+ return wrapValue(val, opts);
231
+ };
232
+ }
233
+ };
234
+ export const parseTypes = (config) => {
235
+ return Object.entries(config.fields).reduce((m, v) => {
236
+ let [key, value] = v;
237
+ let type;
238
+ let from;
239
+ if (typeof value === 'string') {
240
+ type = value;
241
+ from = [key];
242
+ if (['related', 'location'].includes(type)) {
243
+ throw new Error('must use object for ' + type + ' type');
244
+ }
245
+ value = {
246
+ type,
247
+ from
248
+ };
249
+ }
250
+ else {
251
+ type = value.type;
252
+ from = getFromValue(value.from || key);
253
+ }
254
+ m[key] = getCoercionFunc(type, from, value);
255
+ return m;
256
+ }, {});
257
+ };
package/esm/parser.js ADDED
@@ -0,0 +1,59 @@
1
+ // @ts-nocheck
2
+ import { readFileSync } from 'fs';
3
+ import { parse, parseTypes } from './index';
4
+ import { deparse } from 'pgsql-deparser';
5
+ import { InsertOne, InsertMany } from './utils';
6
+ export class Parser {
7
+ constructor(config) {
8
+ this.config = config;
9
+ }
10
+ async parse(data) {
11
+ const config = this.config;
12
+ const { schema, table, singleStmts, conflict, headers, delimeter } = config;
13
+ const opts = {};
14
+ if (headers)
15
+ opts.headers = headers;
16
+ if (delimeter)
17
+ opts.separator = delimeter;
18
+ let records;
19
+ if (typeof data === 'undefined') {
20
+ if (config.json || config.input.endsWith('.json')) {
21
+ records = JSON.parse(readFileSync(config.input, 'utf-8'));
22
+ }
23
+ else {
24
+ records = await parse(config.input, opts);
25
+ }
26
+ }
27
+ else {
28
+ if (!Array.isArray(data)) {
29
+ throw new Error('data is not an array');
30
+ }
31
+ records = data;
32
+ }
33
+ if (config.debug) {
34
+ console.log(records);
35
+ return;
36
+ }
37
+ const types = parseTypes(config);
38
+ if (singleStmts) {
39
+ const stmts = records.map((record) => InsertOne({
40
+ schema,
41
+ table,
42
+ types,
43
+ record,
44
+ conflict
45
+ }));
46
+ return deparse(stmts);
47
+ }
48
+ else {
49
+ const stmt = InsertMany({
50
+ schema,
51
+ table,
52
+ types,
53
+ records,
54
+ conflict
55
+ });
56
+ return deparse([stmt]);
57
+ }
58
+ }
59
+ }