af-db-ts 1.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.
Files changed (44) hide show
  1. package/README.md +3 -0
  2. package/dist/cjs/db.js +199 -0
  3. package/dist/cjs/db.js.map +1 -0
  4. package/dist/cjs/get-value-for-sql.js +256 -0
  5. package/dist/cjs/get-value-for-sql.js.map +1 -0
  6. package/dist/cjs/index.js +20 -0
  7. package/dist/cjs/index.js.map +1 -0
  8. package/dist/cjs/interfaces.js +3 -0
  9. package/dist/cjs/interfaces.js.map +1 -0
  10. package/dist/cjs/sql.js +375 -0
  11. package/dist/cjs/sql.js.map +1 -0
  12. package/dist/cjs/utils.js +36 -0
  13. package/dist/cjs/utils.js.map +1 -0
  14. package/dist/esm/db.js +185 -0
  15. package/dist/esm/db.js.map +1 -0
  16. package/dist/esm/get-value-for-sql.js +251 -0
  17. package/dist/esm/get-value-for-sql.js.map +1 -0
  18. package/dist/esm/index.js +4 -0
  19. package/dist/esm/index.js.map +1 -0
  20. package/dist/esm/interfaces.js +2 -0
  21. package/dist/esm/interfaces.js.map +1 -0
  22. package/dist/esm/sql.js +361 -0
  23. package/dist/esm/sql.js.map +1 -0
  24. package/dist/esm/utils.js +31 -0
  25. package/dist/esm/utils.js.map +1 -0
  26. package/dist/types/db.d.ts +39 -0
  27. package/dist/types/db.d.ts.map +1 -0
  28. package/dist/types/get-value-for-sql.d.ts +7 -0
  29. package/dist/types/get-value-for-sql.d.ts.map +1 -0
  30. package/dist/types/index.d.ts +5 -0
  31. package/dist/types/index.d.ts.map +1 -0
  32. package/dist/types/interfaces.d.ts +181 -0
  33. package/dist/types/interfaces.d.ts.map +1 -0
  34. package/dist/types/sql.d.ts +56 -0
  35. package/dist/types/sql.d.ts.map +1 -0
  36. package/dist/types/utils.d.ts +10 -0
  37. package/dist/types/utils.d.ts.map +1 -0
  38. package/package.json +75 -0
  39. package/src/db.ts +195 -0
  40. package/src/get-value-for-sql.ts +271 -0
  41. package/src/index.ts +47 -0
  42. package/src/interfaces.ts +232 -0
  43. package/src/sql.ts +403 -0
  44. package/src/utils.ts +31 -0
@@ -0,0 +1,271 @@
1
+ import { DateTime } from 'luxon';
2
+ import { getBool, rn } from 'af-tools-ts';
3
+ import { IGetValueForSQLArgs, IPrepareSqlStringArgs } from './interfaces';
4
+ import { prepareSqlString, sql } from './sql';
5
+ import { mssqlEscape, q } from './utils';
6
+
7
+ export const binToHexString = (value: any) => (value ? `0x${value.toString(16).toUpperCase()}` : null);
8
+
9
+ const getTypeOfDateInput = (v: any): 'string' | 'number' | 'date' | 'luxon' | 'moment' | 'any' | 'null' => {
10
+ const type = typeof v;
11
+ if (type === 'string' || type === 'number') {
12
+ return type;
13
+ }
14
+ if (type === 'boolean' || !v) {
15
+ return 'null';
16
+ }
17
+ if (type === 'object') {
18
+ if (Object.prototype.toString.call(v) === '[object Date]') {
19
+ return 'date';
20
+ }
21
+ if (v.isLuxonDateTime) {
22
+ return 'luxon';
23
+ }
24
+ if (v._isAMomentObject) {
25
+ return 'moment';
26
+ }
27
+ }
28
+ return 'any';
29
+ };
30
+
31
+ /**
32
+ * Возвращает значение, готовое для использования в строке SQL запроса
33
+ */
34
+ export const getValueForSQL = (args: IGetValueForSQLArgs): string | number | null => {
35
+ let { value, fieldSchema, escapeOnlySingleQuotes } = args;
36
+ const { dateTimeOptions, needValidate } = args;
37
+ if (typeof fieldSchema === 'string') {
38
+ fieldSchema = { type: fieldSchema };
39
+ }
40
+ const {
41
+ type,
42
+ arrayType,
43
+ length = 0,
44
+ scale,
45
+ nullable = true,
46
+ inputDateFormat,
47
+ defaultValue,
48
+ noQuotes = false,
49
+ name,
50
+ } = fieldSchema;
51
+ let val;
52
+ const IS_POSTGRES = args.dialect === 'pg';
53
+
54
+ if (escapeOnlySingleQuotes == null) {
55
+ ({ escapeOnlySingleQuotes } = fieldSchema);
56
+ }
57
+ if (escapeOnlySingleQuotes == null) {
58
+ escapeOnlySingleQuotes = false;
59
+ }
60
+
61
+ function prepareNumber (min: number, max: number, value_ = value) {
62
+ if (value_ === 'null' || value_ == null || Number.isNaN(value_)) {
63
+ if (nullable) {
64
+ return 'NULL';
65
+ }
66
+ return (defaultValue || defaultValue === 0) ? `${defaultValue}` : null;
67
+ }
68
+ val = Number(value_);
69
+ if (needValidate && (val < min || val > max)) {
70
+ // throwValidateError()
71
+ throw new Error(`Type [${type}] validate error. Value: ${val} / FName: ${name}`);
72
+ }
73
+ return `${val}`;
74
+ }
75
+
76
+ const prepareSqlStringArgs: IPrepareSqlStringArgs = {
77
+ value, nullable, length, defaultValue, noQuotes, escapeOnlySingleQuotes,
78
+ };
79
+ switch (type) {
80
+ case 'json':
81
+ if (Array.isArray(value) || typeof value === 'object') {
82
+ value = JSON.stringify(value);
83
+ }
84
+ return prepareSqlString({ ...prepareSqlStringArgs, value });
85
+
86
+ case 'string':
87
+ case sql.Char:
88
+ case sql.NChar:
89
+ case sql.Text:
90
+ case sql.NText:
91
+ case sql.VarChar:
92
+ case sql.NVarChar:
93
+ case sql.Xml:
94
+ return prepareSqlString(prepareSqlStringArgs);
95
+
96
+ case 'uid':
97
+ case 'uuid':
98
+ case 'uniqueIdentifier':
99
+ case sql.UniqueIdentifier:
100
+ if (!value || typeof value !== 'string' || !/^[A-F\d]{8}(-[A-F\d]{4}){4}[A-F\d]{8}/i.test(value)) {
101
+ value = null;
102
+ } else {
103
+ value = value.substring(0, 36).toUpperCase();
104
+ }
105
+ return prepareSqlString({ ...prepareSqlStringArgs, value, length: 0 });
106
+
107
+ case 'datetime':
108
+ case 'date':
109
+ case 'time':
110
+ case sql.DateTime:
111
+ case sql.DateTime2:
112
+ case sql.Time:
113
+ case sql.Date:
114
+ case sql.SmallDateTime:
115
+ case sql.DateTimeOffset: {
116
+ let millis = 0;
117
+ val = value;
118
+
119
+ let inputType = getTypeOfDateInput(value); // 'string' | 'number' | 'date' | 'luxon' | 'moment' | 'any' | 'null'
120
+
121
+ if (inputType === 'null') {
122
+ if (nullable) {
123
+ return 'NULL';
124
+ }
125
+ inputType = 'number';
126
+ val = 0;
127
+ } else if (inputType === 'any') {
128
+ inputType = 'string';
129
+ val = String(value);
130
+ }
131
+ switch (inputType) {
132
+ case 'number':
133
+ millis = val;
134
+ break;
135
+ case 'date':
136
+ millis = +val;
137
+ break;
138
+ case 'luxon':
139
+ millis = val.isValid ? val.toMillis() : 0;
140
+ break;
141
+ case 'moment':
142
+ millis = val.isValid() ? +val : 0;
143
+ break;
144
+ // string and other
145
+ default: {
146
+ val = String(value);
147
+ millis = (inputDateFormat
148
+ ? DateTime.fromFormat(val, inputDateFormat, dateTimeOptions)
149
+ : DateTime.fromISO(val, dateTimeOptions)).toMillis();
150
+ break;
151
+ }
152
+ }
153
+ millis = Math.max(millis + (dateTimeOptions?.correctionMillis || 0), 0);
154
+ const luxonDate = DateTime.fromMillis(millis);
155
+
156
+ switch (type) {
157
+ case 'datetime':
158
+ case sql.DateTime:
159
+ case sql.DateTime2:
160
+ return q(luxonDate.toISO({ includeOffset: false }) || '', noQuotes); // 2023-09-05T02:23:54.105
161
+ case 'time':
162
+ case sql.Time:
163
+ return q((luxonDate.toISOTime() || '').substring(0, 12), noQuotes); // 02:22:17.368
164
+ case 'date':
165
+ case sql.Date:
166
+ return q(luxonDate.toSQLDate() || '', noQuotes); // 2023-09-05
167
+ case sql.SmallDateTime:
168
+ return q(`${(luxonDate.toISO() || '').substring(0, 17)}00`, noQuotes); // 2023-09-05T02:20:00
169
+ case sql.DateTimeOffset: { // VVQ TESTS
170
+ const dotScale = scale == null ? 3 : scale;
171
+ const re = /\.(\d+)(?=[^.]*$)/;
172
+ let str = luxonDate.toISO({ includeOffset: false }) || '';
173
+ if (!dotScale) {
174
+ str = str.replace(re, `.000`);
175
+ } else {
176
+ val = inputType === 'string' ? value : str;
177
+ let [, fracSeconds = '0'] = re.exec(val) || [];
178
+ let floatSeconds = parseFloat((`1.${fracSeconds}`));
179
+ floatSeconds = rn(floatSeconds, dotScale);
180
+ fracSeconds = (`${floatSeconds}0000000`).substring(2, 2 + dotScale);
181
+ str = str.replace(re, `.${fracSeconds}`);
182
+ }
183
+ return q(str, noQuotes);
184
+ }
185
+ default:
186
+ return q(luxonDate.toISO({ includeOffset: false }) || '', noQuotes); // 2023-09-05T02:23:54.105
187
+ }
188
+ }
189
+
190
+ case 'boolean':
191
+ case sql.Bit: {
192
+ val = getBool(value);
193
+ if (IS_POSTGRES) {
194
+ return val ? 'true' : 'false';
195
+ }
196
+ return val ? '1' : '0';
197
+ }
198
+
199
+ case sql.TinyInt:
200
+ return prepareNumber(0, 255);
201
+ case 'smallint':
202
+ case sql.SmallInt:
203
+ return prepareNumber(-32768, 32767);
204
+ case 'int':
205
+ case sql.Int:
206
+ case 'integer':
207
+ return prepareNumber(-2147483648, 2147483647);
208
+ case sql.BigInt:
209
+ // eslint-disable-next-line no-loss-of-precision
210
+ return prepareNumber(-9223372036854775808, 9223372036854775807);
211
+ case 'number':
212
+ case sql.Decimal:
213
+ case sql.Float:
214
+ case sql.Money:
215
+ case sql.Numeric:
216
+ case sql.SmallMoney:
217
+ case sql.Real:
218
+ if (value == null) {
219
+ if (nullable) {
220
+ return 'NULL';
221
+ }
222
+ return (defaultValue || defaultValue === 0) ? `${defaultValue}` : null;
223
+ }
224
+ return `${value}`;
225
+ case sql.Binary:
226
+ case sql.VarBinary:
227
+ case sql.Image:
228
+ if (value == null) {
229
+ if (nullable) {
230
+ return 'NULL';
231
+ }
232
+ if (!defaultValue) return null;
233
+ }
234
+ return binToHexString(value);
235
+ case sql.UDT:
236
+ case sql.Geography:
237
+ case sql.Geometry:
238
+ case sql.Variant:
239
+ return prepareSqlString(prepareSqlStringArgs);
240
+ case 'array': {
241
+ let arr: any[] = [];
242
+ if (Array.isArray(value) && value.length) {
243
+ switch (arrayType) {
244
+ case 'int':
245
+ case 'integer':
246
+ arr = value.map((v) => prepareNumber(-2147483648, 2147483647, v));
247
+ break;
248
+ default: // + case 'string'
249
+ arr = value.map((v) => {
250
+ if (v === '') {
251
+ return v;
252
+ }
253
+ if (v == null) {
254
+ return null;
255
+ }
256
+ return mssqlEscape(String(value));
257
+ })
258
+ .filter((v) => v != null)
259
+ .map((v) => `"${v}"`);
260
+ break;
261
+ }
262
+ }
263
+ if (arr.length) {
264
+ return `{${arr.join(',')}`;
265
+ }
266
+ return '{}';
267
+ }
268
+ default:
269
+ return prepareSqlString(prepareSqlStringArgs);
270
+ }
271
+ };
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ export * as db from './db';
2
+
3
+ export {
4
+ prepareSqlString,
5
+ correctRecordSchema,
6
+ getRecordSchema,
7
+ wrapTransaction,
8
+ serialize,
9
+ getRecordValuesForSQL,
10
+ getSqlSetExpression,
11
+ getSqlValuesExpression,
12
+ prepareRecordForSQL,
13
+ prepareDataForSQL,
14
+ getRowsAffected,
15
+ } from './sql';
16
+
17
+ export {
18
+ binToHexString,
19
+ getValueForSQL,
20
+ } from './get-value-for-sql';
21
+
22
+ export {
23
+ IDBConfig,
24
+ IFieldSchema,
25
+ IGetMergeSQLOptions,
26
+ TDBRecord,
27
+ TFieldName,
28
+ TFieldTypeCorrection,
29
+ TGetRecordSchemaOptions,
30
+ TRecordSchema,
31
+ TRecordSchemaAssoc,
32
+ TRecordSet,
33
+ TRecordSetAssoc,
34
+ TMergeRules,
35
+ TMergeResult,
36
+ TRecordKey,
37
+ TGetPoolConnectionOptions,
38
+ TGetRecordSchemaResult,
39
+ IPrepareSqlStringArgs,
40
+ IGetValueForSQLArgs,
41
+ IDialect,
42
+ DateTimeOptionsEx,
43
+ IPrepareArgs,
44
+ IPrepareRecordParams,
45
+ ISchemaItem,
46
+ IValueForSQLPartialArgs,
47
+ } from './interfaces';
@@ -0,0 +1,232 @@
1
+ import { ConnectionPool, IColumnMetadata, ISqlType } from 'mssql';
2
+ import { DateTimeOptions } from 'luxon';
3
+
4
+ /**
5
+ * Имя поля БД
6
+ */
7
+ export type TFieldName = string;
8
+
9
+ /**
10
+ * Значение ключевого поля записи БД
11
+ */
12
+ export type TRecordKey = string | number;
13
+
14
+ /**
15
+ * Метаинформация о поле БД
16
+ */
17
+ export interface IFieldSchema {
18
+ index?: number,
19
+ name?: string,
20
+ length?: number,
21
+ type?: any,
22
+ arrayType?: any,
23
+ scale?: number,
24
+ precision?: number,
25
+ nullable?: boolean,
26
+ caseSensitive?: boolean,
27
+ identity?: boolean,
28
+ excludeFromInsert?: boolean,
29
+ readOnly?: boolean,
30
+ inputDateFormat?: string,
31
+ defaultValue?: any,
32
+ noQuotes?: boolean,
33
+ escapeOnlySingleQuotes?: boolean,
34
+ }
35
+
36
+ /**
37
+ * Массив объектов с метаинформацией о полях
38
+ */
39
+ export type TRecordSchema = IFieldSchema[]
40
+
41
+ /**
42
+ * Метаинформацией о полях, проиндексированная именами полей. (sql.recordset.columns)
43
+ */
44
+ export interface TRecordSchemaAssoc {
45
+ [fieldName: TFieldName]: IFieldSchema
46
+ }
47
+
48
+ /**
49
+ * Объект корректировки типов полей. Наименованию поля соответствует новый тип.
50
+ *
51
+ * В частности, используется для полей, хранящих знаяения типа json в поле типа varchar(max)
52
+ * тогда необходимо явно задать тип поля "json". Если имя поля заканчивается на _json, коррекция типа произойдет автоматически.
53
+ * Также используется для указания входного формата для преобразования строки в тип datetime (свойство inputDateFormat в схеме поля)
54
+ */
55
+ export interface TFieldTypeCorrection {
56
+ [fieldName: TFieldName]: IFieldSchema
57
+ }
58
+
59
+ // =============================== records =====================================
60
+
61
+ /**
62
+ * Запись БД. Объект, проиндексированный именами полей. Значения - значения полей
63
+ */
64
+ export interface TDBRecord {
65
+ [fieldName: TFieldName]: any
66
+ }
67
+
68
+ /**
69
+ * Массив записей БД
70
+ *
71
+ * _isPreparedForSQL - Признак того, что значения полей подготовлены для использования в строке SQL
72
+ */
73
+ export type TRecordSet = TDBRecord[] & { _isPreparedForSQL?: boolean }
74
+
75
+ /**
76
+ * Пакет записей БД.
77
+ * Объект, проиндексированный алиасами. Каждый подобъект содержит TDBRecord.
78
+ */
79
+ export interface TRecordSetAssoc {
80
+ [recordKey: TRecordKey]: TDBRecord
81
+ }
82
+
83
+ export interface TMergeResult {
84
+ // кол-во затронутых записей
85
+ total: number,
86
+ // кол-во добавленных записей
87
+ inserted: number,
88
+ // кол-во измененных записей
89
+ updated: number,
90
+ }
91
+
92
+ /**
93
+ * Метаинформация для формирования инструкции SQL MERGE
94
+ */
95
+ export interface TMergeRules {
96
+ // массив имен полей, идентифицирующих запись, используемый в выражении ON в MERGE
97
+ mergeIdentity?: TFieldName[],
98
+ // массив имен полей, исключаемых из списка при вставке в MERGE. Обычно это автоинкрементное поле.
99
+ excludeFromInsert?: TFieldName[],
100
+ // если true - старые не нулевые значения полей не будут перезаписаны нулами при апдейте
101
+ noUpdateIfNull?: boolean,
102
+ correction?: Function,
103
+ withClause?: string,
104
+ }
105
+
106
+ export interface TGetRecordSchemaOptions {
107
+ // массив имен полей, которые нужно удалить из схемы (не уитывается, если asArray = false)
108
+ omitFields?: string[],
109
+ // массив имен полей, которые нужно оставить в схеме
110
+ pickFields?: string[],
111
+ // кол-во измененных записей
112
+ fieldTypeCorrection?: TFieldTypeCorrection,
113
+ mergeRules?: TMergeRules,
114
+ noReturnMergeResult?: boolean,
115
+ }
116
+
117
+ export interface TGetPoolConnectionOptions {
118
+ // Префикс в сообщении о закрытии пула (название синхронизации)
119
+ prefix?: string,
120
+ // Что делать при ошибке соединения:
121
+ // 'exit' - завершить скрипт,
122
+ // 'throw' - бросить исключение.
123
+ // Если не задано - только сообщать в консоль.
124
+ onError?: 'exit' | 'throw'
125
+ errorCode?: number
126
+ }
127
+
128
+ export type IDialect = 'mssql' | 'pg';
129
+
130
+ interface IDBConfigCommon {
131
+ dialect: IDialect,
132
+ port: string | number | null,
133
+ database: string,
134
+ user: string,
135
+ password: string,
136
+ }
137
+
138
+ interface IDBConfigMSSQL extends IDBConfigCommon {
139
+ server: string,
140
+ }
141
+
142
+ interface IDBConfigPG extends IDBConfigCommon {
143
+ host: string,
144
+ }
145
+
146
+ export type IDBConfig = IDBConfigMSSQL | IDBConfigPG
147
+
148
+ export interface ISchemaItem {
149
+ index: number;
150
+ name: string;
151
+ length: number;
152
+ type: (() => ISqlType) | ISqlType;
153
+ udt?: any;
154
+ scale?: number | undefined;
155
+ precision?: number | undefined;
156
+ nullable: boolean;
157
+ caseSensitive: boolean;
158
+ identity: boolean;
159
+ readOnly: boolean;
160
+ }
161
+
162
+ export interface IPrepareSqlStringArgs {
163
+ // Значение, которое нужно подготовить для передачи в SQL
164
+ value: string | number | null,
165
+ // Значение, которое будет подставлено, если передано null | undefined и nullable = false
166
+ defaultValue?: string | null | undefined,
167
+ // Ограничение на длину поля. Если передано, строка урезается
168
+ length?: number,
169
+ // Подставлять NULL, если значение пусто или пустая строка.
170
+ nullable?: boolean | number,
171
+ noQuotes?: boolean,
172
+ escapeOnlySingleQuotes?: boolean,
173
+ }
174
+
175
+ export interface DateTimeOptionsEx extends DateTimeOptions {
176
+ correctionMillis: number,
177
+ }
178
+
179
+ export interface IValueForSQLPartialArgs {
180
+ dateTimeOptions?: DateTimeOptionsEx,
181
+ dialect?: IDialect
182
+ escapeOnlySingleQuotes?: boolean,
183
+ needValidate?: boolean, // Флаг необходимости валидации значения
184
+ }
185
+
186
+ export interface IPrepareRecordParams {
187
+ // Если TRUE - в записи добавляются пропущенные поля со значениями NULL, '', ...
188
+ addMissingFields?: boolean,
189
+ // Для полей, не допускающих NULL будет добавлено наиболее подходящее значение
190
+ addValues4NotNullableFields?: boolean,
191
+ }
192
+
193
+ export interface IGetValueForSQLArgs extends IValueForSQLPartialArgs {
194
+ value: any,
195
+ fieldSchema: IFieldSchema | string,
196
+ }
197
+
198
+ export interface IPrepareArgs extends IValueForSQLPartialArgs, IPrepareRecordParams {
199
+ // объект описания структуры таблицы
200
+ recordSchema: TRecordSchema,
201
+ }
202
+
203
+ export interface IGetMergeSQLOptions extends IValueForSQLPartialArgs, IPrepareRecordParams {
204
+ isPrepareForSQL?: boolean,
205
+ }
206
+
207
+ export interface TGetRecordSchemaResult {
208
+
209
+ connectionId: string,
210
+ dbConfig: IDBConfig,
211
+ schemaAndTable: string,
212
+ dbSchemaAndTable: string,
213
+ columns: IColumnMetadata,
214
+ schemaAssoc: Partial<IColumnMetadata>,
215
+ schema: ISchemaItem[],
216
+ fields: string[],
217
+ insertFields: string[],
218
+ insertFieldsList: string,
219
+ withClause: string | undefined,
220
+ updateFields: string[],
221
+ mergeIdentity: string[],
222
+
223
+ getMergeSQL: (_packet: TRecordSet, _prepareOptions?: IGetMergeSQLOptions) => string,
224
+
225
+ getInsertSQL: (_packet: TRecordSet, _addOutputInserted?: boolean) => string,
226
+
227
+ getUpdateSQL: (_record: TRecordSet) => string,
228
+ }
229
+
230
+ export interface IConnectionPools {
231
+ [poolId: string]: ConnectionPool
232
+ }