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
package/src/sql.ts ADDED
@@ -0,0 +1,403 @@
1
+ // noinspection SqlResolve
2
+ import * as sql from 'mssql';
3
+ import { IColumnMetadata, IResult } from 'mssql';
4
+ import * as _ from 'lodash';
5
+ import { echo } from 'af-echo-ts';
6
+ import * as cache from 'memory-cache';
7
+ import * as db from './db';
8
+ import { q, mssqlEscape } from './utils';
9
+ import { getValueForSQL } from './get-value-for-sql';
10
+ import { IDBConfig,
11
+ IFieldSchema,
12
+ IGetMergeSQLOptions,
13
+ IGetValueForSQLArgs, IPrepareArgs,
14
+ IPrepareSqlStringArgs,
15
+ ISchemaItem,
16
+ TDBRecord,
17
+ TFieldName,
18
+ TFieldTypeCorrection,
19
+ TGetRecordSchemaResult,
20
+ TGetRecordSchemaOptions,
21
+ TRecordSchema,
22
+ TRecordSchemaAssoc,
23
+ TRecordSet } from './interfaces';
24
+
25
+ export { sql };
26
+
27
+ /**
28
+ * Подготовка строки для передачи в SQL
29
+ */
30
+ export const prepareSqlString = (args: IPrepareSqlStringArgs): string | null => {
31
+ const { value, defaultValue = null, length = 0, nullable = false, noQuotes = false, escapeOnlySingleQuotes = false } = args;
32
+ if (value == null) {
33
+ if (nullable) {
34
+ return 'NULL';
35
+ }
36
+ if (defaultValue) {
37
+ return q(defaultValue, noQuotes);
38
+ }
39
+ return ''; // Это нештатная ситуация, т.к. поле не получит никакого значения ( ,, )
40
+ }
41
+ if (value === '') {
42
+ if (noQuotes) {
43
+ return ''; // Это нештатная ситуация, т.к. поле не получит никакого значения ( ,, )
44
+ }
45
+ return `''`;
46
+ }
47
+ let val = mssqlEscape(String(value), escapeOnlySingleQuotes);
48
+ if (length > 0) {
49
+ val = val.substring(0, length);
50
+ }
51
+ return q(val, noQuotes);
52
+ };
53
+
54
+ const FIELD_SCHEMA_PROPS = ['index', 'name', 'length', 'type', 'scale', 'precision', 'nullable', 'caseSensitive',
55
+ 'identity', 'mergeIdentity', 'readOnly', 'inputDateFormat', 'defaultValue'];
56
+
57
+ /**
58
+ * Корректировка схемы таблицы
59
+ * Поля с суффиксом _json получают тип "json". Остальные корректировки берутся из fieldTypeCorrection
60
+ * Например, для полей типа datetime можно передавать свойство inputDateFormat
61
+ */
62
+ export const correctRecordSchema = (
63
+ recordSchemaAssoc: TRecordSchemaAssoc,
64
+ // объект корректировок
65
+ fieldTypeCorrection?: TFieldTypeCorrection,
66
+ ) => {
67
+ _.each(recordSchemaAssoc, (fieldSchema: IFieldSchema, fieldName: TFieldName) => {
68
+ if (/_json$/i.test(fieldName)) {
69
+ fieldSchema.type = 'json';
70
+ }
71
+ switch (fieldSchema.type) {
72
+ case sql.NChar:
73
+ case sql.NText:
74
+ case sql.NVarChar:
75
+ if (fieldSchema.length) {
76
+ fieldSchema.length = Math.floor(fieldSchema.length / 2);
77
+ }
78
+ break;
79
+ case sql.UniqueIdentifier:
80
+ fieldSchema.length = 36;
81
+ break;
82
+ default:
83
+ }
84
+ });
85
+ if (fieldTypeCorrection && typeof fieldTypeCorrection === 'object') {
86
+ _.each(fieldTypeCorrection, (correction: IFieldSchema, fieldName: TFieldName) => {
87
+ FIELD_SCHEMA_PROPS.forEach((prop) => {
88
+ if (correction[prop] !== undefined) {
89
+ if (!recordSchemaAssoc[fieldName]) {
90
+ recordSchemaAssoc[fieldName] = {} as IFieldSchema;
91
+ }
92
+ recordSchemaAssoc[fieldName][prop] = correction[prop];
93
+ }
94
+ });
95
+ });
96
+ }
97
+ };
98
+
99
+ /**
100
+ * Подготовка значений записи для использования в SQL
101
+ *
102
+ * Все поля записи обрабатываются функцией getValueForSQL
103
+ */
104
+ export const prepareRecordForSQL = (record: TDBRecord, args: IPrepareArgs) => {
105
+ const { addValues4NotNullableFields, addMissingFields } = args;
106
+ const { dateTimeOptions, needValidate, escapeOnlySingleQuotes, dialect } = args;
107
+ const options: IGetValueForSQLArgs = {
108
+ value: null, fieldSchema: '', dateTimeOptions, needValidate, escapeOnlySingleQuotes, dialect,
109
+ };
110
+ args.recordSchema.forEach((fieldSchema: IFieldSchema) => {
111
+ const { name = '_#foo#_', readOnly } = fieldSchema;
112
+ if (readOnly) {
113
+ return;
114
+ }
115
+ if (Object.prototype.hasOwnProperty.call(record, name)) {
116
+ record[name] = getValueForSQL({ ...options, value: record[name], fieldSchema });
117
+ } else if ((!fieldSchema.nullable && addValues4NotNullableFields) || addMissingFields) {
118
+ record[name] = getValueForSQL({ ...options, value: null, fieldSchema });
119
+ }
120
+ });
121
+ };
122
+
123
+ /**
124
+ * Подготовка данных для SQL
125
+ *
126
+ * Все поля всех записей обрабатываются функцией getValueForSQL
127
+ */
128
+ export const prepareDataForSQL = (recordSet: TRecordSet, args: IPrepareArgs) => {
129
+ if (recordSet._isPreparedForSQL) {
130
+ return;
131
+ }
132
+ recordSet.forEach((record) => {
133
+ prepareRecordForSQL(record, args);
134
+ });
135
+ recordSet._isPreparedForSQL = true;
136
+ };
137
+
138
+ /**
139
+ * Возвращает рекорд, в котором все значения преобразованы в строки и подготовлены для прямой вставки в SQL
140
+ * В частности, если значение типа строка, то оно уже заключено в одинарные кавычки
141
+ */
142
+ export const getRecordValuesForSQL = (record: TDBRecord, recordSchema: TRecordSchema): TDBRecord => {
143
+ const recordValuesForSQL = {};
144
+ recordSchema.forEach((fieldSchema) => {
145
+ const { name = '_#foo#_', readOnly } = fieldSchema;
146
+ if (readOnly) {
147
+ return;
148
+ }
149
+ if (Object.prototype.hasOwnProperty.call(record, name)) {
150
+ recordValuesForSQL[name] = getValueForSQL({ value: record[name], fieldSchema, escapeOnlySingleQuotes: true });
151
+ }
152
+ });
153
+ return recordValuesForSQL;
154
+ };
155
+
156
+ /**
157
+ * Возвращает схему полей таблицы БД. Либо в виде объекта, либо в виде массива
158
+ * Если asArray = true, то вернет TRecordSchema, при этом удалит поля, указанные в omitFields
159
+ * Иначе вернет TRecordSchemaAssoc
160
+ */
161
+ export const getRecordSchema = async (
162
+ // ID соединения (borf|cep|hr|global)
163
+ connectionId: string,
164
+ // Субъект в выражении FROM для таблицы, схему которой нужно вернуть
165
+ schemaAndTable: string,
166
+ // Массив имен полей, которые нужно удалить из схемы (не учитывается, если asArray = false)
167
+ options: TGetRecordSchemaOptions = {} as TGetRecordSchemaOptions,
168
+ ): Promise<TGetRecordSchemaResult | undefined> => {
169
+ const propertyPath = `schemas.${connectionId}.${schemaAndTable}`;
170
+
171
+ let result: TGetRecordSchemaResult | undefined = cache.get(propertyPath) as TGetRecordSchemaResult | undefined;
172
+ if (result) {
173
+ return result;
174
+ }
175
+ const {
176
+ omitFields,
177
+ pickFields,
178
+ fieldTypeCorrection,
179
+ mergeRules: {
180
+ mergeIdentity = [],
181
+ excludeFromInsert = [],
182
+ noUpdateIfNull = false,
183
+ correction: mergeCorrection,
184
+ withClause,
185
+ } = {},
186
+ noReturnMergeResult,
187
+ } = options;
188
+ const cPool = await db.getPoolConnection(connectionId, { prefix: 'getRecordSchema' });
189
+ const request = new sql.Request(cPool);
190
+ request.stream = false;
191
+ let res: IResult<any>;
192
+ try {
193
+ res = await request.query(`SELECT TOP(1) *
194
+ FROM ${schemaAndTable}`);
195
+ } catch (err) {
196
+ echo.error(`getRecordSchema SQL ERROR`);
197
+ echo.error(err);
198
+ throw err;
199
+ }
200
+ const { columns } = res.recordset;
201
+ const readOnlyFields = Object.entries(columns).filter(([, { readOnly: ro }]) => ro).map(([f]) => f);
202
+ const omitFields2 = [...readOnlyFields, ...(Array.isArray(omitFields) ? omitFields : [])];
203
+ let schemaAssoc: Partial<IColumnMetadata> = _.omit<IColumnMetadata>(columns, omitFields2);
204
+ schemaAssoc = Array.isArray(pickFields) ? _.pick(schemaAssoc, pickFields) : schemaAssoc;
205
+ correctRecordSchema(schemaAssoc as TRecordSchemaAssoc, fieldTypeCorrection);
206
+ const schema: ISchemaItem[] = _.map(schemaAssoc, (fo) => (fo))
207
+ .sort((a, b) => {
208
+ const ai = (a?.index || 0);
209
+ const bi = (b?.index || 0);
210
+ if (ai > bi) return 1;
211
+ if (ai < bi) return -1;
212
+ return 0;
213
+ }) as ISchemaItem[];
214
+ const fields = schema.map((o) => o?.name).filter(Boolean) as string[];
215
+ const fieldsList = fields.map((fName) => `[${fName}]`)
216
+ .join(', ');
217
+
218
+ const onClause = `(${mergeIdentity.map((fName) => (`target.[${fName}] = source.[${fName}]`))
219
+ .join(' AND ')})`;
220
+ const insertFields = fields.filter((fName) => (!excludeFromInsert.includes(fName)));
221
+ const insertSourceList = insertFields.map((fName) => (`source.[${fName}]`))
222
+ .join(', ');
223
+ const insertFieldsList = insertFields.map((fName) => `[${fName}]`)
224
+ .join(', ');
225
+ const updateFields = fields.filter((fName) => (!mergeIdentity.includes(fName)));
226
+ let updateFieldsList: string;
227
+ if (noUpdateIfNull) {
228
+ updateFieldsList = updateFields.map((fName) => (`target.[${fName}] = COALESCE(source.[${fName}], target.[${fName}])`)).join(', ');
229
+ } else {
230
+ updateFieldsList = updateFields.map((fName) => (`target.[${fName}] = source.[${fName}]`)).join(', ');
231
+ }
232
+ const dbConfig: IDBConfig = db.getDbConfig(connectionId) as IDBConfig;
233
+ const dbSchemaAndTable = `[${dbConfig.database}].${schemaAndTable}`;
234
+
235
+ result = {
236
+ connectionId,
237
+ dbConfig,
238
+ schemaAndTable,
239
+ dbSchemaAndTable,
240
+ columns,
241
+ schemaAssoc,
242
+ schema,
243
+ fields,
244
+ insertFields,
245
+ insertFieldsList,
246
+ withClause,
247
+ updateFields,
248
+ mergeIdentity,
249
+ getMergeSQL (packet: TRecordSet, prepareOptions: IGetMergeSQLOptions = {}): string {
250
+ if (prepareOptions.isPrepareForSQL) {
251
+ prepareDataForSQL(packet, { recordSchema: this.schema, ...prepareOptions });
252
+ }
253
+ const values = `(${packet.map((r) => (fields.map((fName) => (r[fName]))
254
+ .join(',')))
255
+ .join(`)\n,(`)})`;
256
+ let mergeSQL = `
257
+ MERGE ${schemaAndTable} ${withClause || ''} AS target
258
+ USING
259
+ (
260
+ SELECT * FROM
261
+ ( VALUES
262
+ ${values}
263
+ )
264
+ AS s (
265
+ ${fieldsList}
266
+ )
267
+ )
268
+ AS source
269
+ ON ${onClause}
270
+ WHEN MATCHED THEN
271
+ UPDATE SET
272
+ ${updateFieldsList}
273
+ WHEN NOT MATCHED THEN
274
+ INSERT (
275
+ ${insertFieldsList}
276
+ )
277
+ VALUES (
278
+ ${insertSourceList}
279
+ )`;
280
+ if (!noReturnMergeResult) {
281
+ mergeSQL = `
282
+ ${'DECLARE'} @t TABLE ( act VARCHAR(20));
283
+ DECLARE @total AS INTEGER;
284
+ DECLARE @i AS INTEGER;
285
+ DECLARE @u AS INTEGER;
286
+ ${mergeSQL}
287
+ OUTPUT $action INTO @t;
288
+ SET @total = @@ROWCOUNT;
289
+ SELECT @i = COUNT(*) FROM @t WHERE act = 'INSERT';
290
+ SELECT @u = COUNT(*) FROM @t WHERE act != 'INSERT';
291
+ SELECT @total as total, @i as inserted, @u as updated;
292
+ `;
293
+ } else {
294
+ mergeSQL += `;\n`;
295
+ }
296
+ return typeof mergeCorrection === 'function' ? mergeCorrection(mergeSQL) : mergeSQL;
297
+ },
298
+
299
+ getInsertSQL (packet: TRecordSet, addOutputInserted = false): string {
300
+ if (!Array.isArray(packet)) {
301
+ packet = [packet];
302
+ }
303
+ const values = `(${packet.map((r) => (insertFields.map((fName) => (r[fName] === undefined ? 'NULL' : r[fName]))
304
+ .join(',')))
305
+ .join(`)\n,(`)})`;
306
+ return `INSERT INTO ${schemaAndTable} (${insertFieldsList}) ${addOutputInserted ? ' OUTPUT inserted.* ' : ''} VALUES ${values}`;
307
+ },
308
+
309
+ getUpdateSQL (record: TRecordSet) {
310
+ const recordForSQL = getRecordValuesForSQL(record, this.schema);
311
+ const setArray: string[] = [];
312
+ updateFields.forEach((fName) => {
313
+ if (recordForSQL[fName] !== undefined) {
314
+ setArray.push(`[${fName}] = ${recordForSQL[fName]}`);
315
+ }
316
+ });
317
+ const where = `(${mergeIdentity.map((fName) => (`[${fName}] = ${recordForSQL[fName]}`))
318
+ .join(' AND ')})`;
319
+ return `UPDATE ${schemaAndTable}
320
+ SET ${setArray.join(', ')}
321
+ WHERE ${where};`;
322
+ },
323
+ };
324
+
325
+ cache.put(propertyPath, result);
326
+ return result;
327
+ };
328
+
329
+ /**
330
+ * Оборачивает инструкции SQL в транзакцию
331
+ */
332
+ export const wrapTransaction = (strSQL: string): string => `BEGIN TRY
333
+ BEGIN TRANSACTION;
334
+
335
+ ${strSQL}
336
+
337
+ COMMIT TRANSACTION;
338
+ END TRY
339
+ BEGIN CATCH
340
+ DECLARE @ErrorMessage NVARCHAR(MAX)
341
+ , @ErrorSeverity INT
342
+ , @ErrorState INT;
343
+
344
+ SELECT
345
+ @ErrorMessage = ERROR_MESSAGE() + ' Line ' + CAST(ERROR_LINE() AS NVARCHAR(5))
346
+ , @ErrorSeverity = ERROR_SEVERITY()
347
+ , @ErrorState = ERROR_STATE();
348
+
349
+ IF @@trancount > 0
350
+ BEGIN
351
+ ROLLBACK TRANSACTION;
352
+ END;
353
+
354
+ RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState);
355
+ END CATCH;`;
356
+
357
+ /**
358
+ * Возвращает проверенное и серилизованное значение
359
+ */
360
+ export const serialize = (value: any, fieldSchema: IFieldSchema): string | number | null => {
361
+ const val = getValueForSQL({ value, fieldSchema });
362
+ if (val == null || val === 'NULL') {
363
+ return null;
364
+ }
365
+ if (typeof val === 'number') {
366
+ return val;
367
+ }
368
+ return String(val).replace(/(^')|('$)/g, '');
369
+ };
370
+
371
+ /**
372
+ * Возвращает подготовленное выражение SET для использования в UPDATE
373
+ */
374
+ export const getSqlSetExpression = (record: TDBRecord, recordSchema: TRecordSchema): string => {
375
+ const setArray: string[] = [];
376
+ recordSchema.forEach((fieldSchema) => {
377
+ const { name = '_#foo#_' } = fieldSchema;
378
+ if (Object.prototype.hasOwnProperty.call(record, name)) {
379
+ setArray.push(`[${name}] = ${getValueForSQL({ value: record[name], fieldSchema, escapeOnlySingleQuotes: true })}`);
380
+ }
381
+ });
382
+ return `SET ${setArray.join(', ')}`;
383
+ };
384
+
385
+ /**
386
+ * Возвращает подготовленное выражение (...поля...) VALUES (...значения...) для использования в INSERT
387
+ *
388
+ * addOutputInserted - Если true, добавляется выражение OUTPUT inserted.* перед VALUES
389
+ */
390
+ export const getSqlValuesExpression = (record: TDBRecord, recordSchema: TRecordSchema, addOutputInserted: boolean = false): string => {
391
+ const fieldsArray: string[] = [];
392
+ const valuesArray: string[] = [];
393
+ recordSchema.forEach((fieldSchema) => {
394
+ const { name = '_#foo#_' } = fieldSchema;
395
+ if (Object.prototype.hasOwnProperty.call(record, name)) {
396
+ fieldsArray.push(name);
397
+ valuesArray.push(String(getValueForSQL({ value: record[name], fieldSchema, escapeOnlySingleQuotes: true })));
398
+ }
399
+ });
400
+ return `([${fieldsArray.join('], [')}]) ${addOutputInserted ? ' OUTPUT inserted.* ' : ''} VALUES (${valuesArray.join(', ')})`;
401
+ };
402
+
403
+ export const getRowsAffected = (qResult: any) => (qResult.rowsAffected && qResult.rowsAffected.reduce((a: number, v: number) => a + v, 0)) || 0;
package/src/utils.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Оборачивает строку в одинарные кавычки, если второй аргумент не true
3
+ */
4
+ export const q = (val: string, noQuotes?: boolean): string => (noQuotes ? val : `'${val}'`);
5
+
6
+ /**
7
+ * Экранирование одинарной кавычки и символа % для использования строки в SQL запросе
8
+ * onlySingleQuotes - true - не экранировать %
9
+ */
10
+ export const mssqlEscape = (str: any, onlySingleQuotes: boolean = false): string => {
11
+ if (str == null) {
12
+ str = '';
13
+ }
14
+ switch (typeof str) {
15
+ case 'number':
16
+ str = String(str);
17
+ break;
18
+ case 'string':
19
+ break;
20
+ case 'boolean':
21
+ str = str ? '1' : '0';
22
+ break;
23
+ default:
24
+ str = String(str || '');
25
+ }
26
+ str = str.replace(/'/g, `''`);
27
+ if (onlySingleQuotes) {
28
+ return str;
29
+ }
30
+ return str.replace(/%/g, '%%');
31
+ };