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.
- package/README.md +3 -0
- package/dist/cjs/db.js +199 -0
- package/dist/cjs/db.js.map +1 -0
- package/dist/cjs/get-value-for-sql.js +256 -0
- package/dist/cjs/get-value-for-sql.js.map +1 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/interfaces.js +3 -0
- package/dist/cjs/interfaces.js.map +1 -0
- package/dist/cjs/sql.js +375 -0
- package/dist/cjs/sql.js.map +1 -0
- package/dist/cjs/utils.js +36 -0
- package/dist/cjs/utils.js.map +1 -0
- package/dist/esm/db.js +185 -0
- package/dist/esm/db.js.map +1 -0
- package/dist/esm/get-value-for-sql.js +251 -0
- package/dist/esm/get-value-for-sql.js.map +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/interfaces.js +2 -0
- package/dist/esm/interfaces.js.map +1 -0
- package/dist/esm/sql.js +361 -0
- package/dist/esm/sql.js.map +1 -0
- package/dist/esm/utils.js +31 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/types/db.d.ts +39 -0
- package/dist/types/db.d.ts.map +1 -0
- package/dist/types/get-value-for-sql.d.ts +7 -0
- package/dist/types/get-value-for-sql.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/interfaces.d.ts +181 -0
- package/dist/types/interfaces.d.ts.map +1 -0
- package/dist/types/sql.d.ts +56 -0
- package/dist/types/sql.d.ts.map +1 -0
- package/dist/types/utils.d.ts +10 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +75 -0
- package/src/db.ts +195 -0
- package/src/get-value-for-sql.ts +271 -0
- package/src/index.ts +47 -0
- package/src/interfaces.ts +232 -0
- package/src/sql.ts +403 -0
- 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
|
+
};
|