-
Notifications
You must be signed in to change notification settings - Fork 910
/
utils.ts
846 lines (732 loc) · 28.9 KB
/
utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as azdataType from 'azdata';
import * as vscode from 'vscode';
import * as os from 'os';
import * as constants from './constants';
import * as path from 'path';
import * as glob from 'fast-glob';
import * as dataworkspace from 'dataworkspace';
import * as mssql from 'mssql';
import * as vscodeMssql from 'vscode-mssql';
import * as fse from 'fs-extra';
import * as which from 'which';
import { promises as fs } from 'fs';
import { ISqlProject, SqlTargetPlatform } from 'sqldbproj';
import { SystemDatabase } from './typeHelper';
export interface ValidationResult {
errorMessage: string;
validated: boolean
}
/**
* Consolidates on the error message string
*/
export function getErrorMessage(error: any): string {
return (error instanceof Error)
? (typeof error.message === 'string' ? error.message : '')
: typeof error === 'string' ? error : `${JSON.stringify(error, undefined, '\t')}`;
}
/**
* removes any leading portion shared between the two URIs from outerUri.
* e.g. [@param innerUri: 'this\is'; @param outerUri: '\this\is\my\path'] => 'my\path' OR
* e.g. [@param innerUri: 'this\was'; @param outerUri: '\this\is\my\path'] => '..\is\my\path'
* @param innerUri the URI that will be cut away from the outer URI
* @param outerUri the URI that will have any shared beginning portion removed
*/
export function trimUri(innerUri: vscode.Uri, outerUri: vscode.Uri): string {
let innerParts = innerUri.path.split('/');
let outerParts = outerUri.path.split('/');
if (path.isAbsolute(outerUri.path)
&& innerParts.length > 0 && outerParts.length > 0
&& innerParts[0].toLowerCase() !== outerParts[0].toLowerCase()) {
throw new Error(constants.outsideFolderPath);
}
while (innerParts.length > 0 && outerParts.length > 0 && innerParts[0].toLocaleLowerCase() === outerParts[0].toLocaleLowerCase()) {
innerParts = innerParts.slice(1);
outerParts = outerParts.slice(1);
}
while (innerParts.length > 1) {
outerParts.unshift(constants.RelativeOuterPath);
innerParts = innerParts.slice(1);
}
return outerParts.join('/');
}
/**
* Trims any character contained in @param chars from both the beginning and end of @param input
*/
export function trimChars(input: string, chars: string): string {
let output = input;
let i = 0;
while (chars.includes(output[i])) { i++; }
output = output.substr(i);
i = 0;
while (chars.includes(output[output.length - i - 1])) { i++; }
output = output.substring(0, output.length - i);
return output;
}
/**
* Ensures that folder path terminates with the slash.
* By default SSDT-style slash (`\`) is used.
*
* @param path Folder path to ensure trailing slash for.
* @param slashCharacter Slash character to ensure is present at the end of the path.
* @returns Path that ends with the given slash character.
*/
export function ensureTrailingSlash(path: string, slashCharacter: string = constants.SqlProjPathSeparator): string {
return path.endsWith(slashCharacter) ? path : path + slashCharacter;
}
/**
* Checks if the folder or file exists @param path path of the folder/file
*/
export async function exists(path: string): Promise<boolean> {
try {
await fs.access(path);
return true;
} catch {
return false;
}
}
/**
* get quoted path to be used in any commandline argument
* @param filePath
*/
export function getQuotedPath(filePath: string): string {
return (os.platform() === 'win32') ?
getQuotedWindowsPath(filePath) :
getQuotedNonWindowsPath(filePath);
}
/**
* ensure that path with spaces are handles correctly (return quoted path)
*/
function getQuotedWindowsPath(filePath: string): string {
filePath = filePath.split('\\').join('\\\\').split('"').join('');
return '"' + filePath + '"';
}
/**
* ensure that path with spaces are handles correctly (return quoted path)
*/
function getQuotedNonWindowsPath(filePath: string): string {
filePath = filePath.split('\\').join('/').split('"').join('');
return '"' + filePath + '"';
}
/**
* Get safe relative path for Windows and non-Windows Platform
* This is needed to read sqlproj entried created on SSDT and opened in MAC
* '/' in tree is recognized all platforms but "\\" only by windows
*
* @param filePath Path to the file or folder.
*/
export function getPlatformSafeFileEntryPath(filePath: string): string {
return filePath.includes('\\')
? filePath.split('\\').join('/')
: filePath;
}
/**
* Standardizes slashes to be "\" for consistency between platforms and compatibility with SSDT
*
* @param filePath Path to the file of folder.
*/
export function convertSlashesForSqlProj(filePath: string): string {
return filePath.includes('/')
? filePath.split('/').join(constants.SqlProjPathSeparator)
: filePath;
}
/**
* Converts a SystemDatabase enum to its string value
* @param systemDb
* @returns
*/
export function systemDatabaseToString(systemDb: SystemDatabase): string {
if (systemDb === mssql.SystemDatabase.Master || systemDb === vscodeMssql.SystemDatabase.Master) {
return constants.master;
} else {
return constants.msdb;
}
}
export function getSystemDatabase(name: string): SystemDatabase {
if (getAzdataApi()) {
return name === constants.master ? mssql.SystemDatabase.Master : mssql.SystemDatabase.MSDB;
} else {
return name === constants.master ? vscodeMssql.SystemDatabase.Master : vscodeMssql.SystemDatabase.MSDB;
}
}
/**
* Read SQLCMD variables from xmlDoc and return them
* @param xmlDoc xml doc to read SQLCMD variables from. Format must be the same that sqlproj and publish profiles use
* @param publishProfile true if reading from publish profile
*/
export function readSqlCmdVariables(xmlDoc: Document, publishProfile: boolean): Map<string, string> {
let sqlCmdVariables: Map<string, string> = new Map();
for (let i = 0; i < xmlDoc.documentElement.getElementsByTagName(constants.SqlCmdVariable)?.length; i++) {
const sqlCmdVar = xmlDoc.documentElement.getElementsByTagName(constants.SqlCmdVariable)[i];
const varName = sqlCmdVar.getAttribute(constants.Include)!;
// Publish profiles only support Value, so don't use DefaultValue even if it's there
// SSDT uses the Value (like <Value>$(SqlCmdVar__1)</Value>) where there
// are local variable values you can set in VS in the properties. Since we don't support that in ADS, only DefaultValue is supported for sqlproj.
if (!publishProfile && sqlCmdVar.getElementsByTagName(constants.DefaultValue)[0] !== undefined) {
// project file path
sqlCmdVariables.set(varName, sqlCmdVar.getElementsByTagName(constants.DefaultValue)[0].childNodes[0].nodeValue!);
}
else {
// profile path
sqlCmdVariables.set(varName, sqlCmdVar.getElementsByTagName(constants.Value)[0].childNodes[0].nodeValue!);
}
}
return sqlCmdVariables;
}
/**
* Removes $() around a sqlcmd variable
* @param name
*/
export function removeSqlCmdVariableFormatting(name: string | undefined): string {
if (!name || name === '') {
return '';
}
if (name.length > 3) {
// Trim in case we get " $(x)"
name = name.trim();
let indexStart = name.startsWith('$(') ? 2 : 0;
let indexEnd = name.endsWith(')') ? 1 : 0;
if (indexStart > 0 || indexEnd > 0) {
name = name.substr(indexStart, name.length - indexEnd - indexStart);
}
}
// Trim in case the customer types " $(x )"
return name.trim();
}
/**
* Format as sqlcmd variable by adding $() if necessary
* if the variable already starts with $(, then add )
* @param name
*/
export function formatSqlCmdVariable(name: string): string {
if (!name || name === '') {
return name;
}
// Trim in case we get " $(x)"
name = name.trim();
if (!name.startsWith('$(') && !name.endsWith(')')) {
name = `$(${name})`;
} else if (name.startsWith('$(') && !name.endsWith(')')) {
// add missing end parenthesis, same behavior as SSDT
name = `${name})`;
}
return name;
}
/**
* Checks if it's a valid sqlcmd variable name
* https://docs.microsoft.com/en-us/sql/ssms/scripting/sqlcmd-use-with-scripting-variables?redirectedfrom=MSDN&view=sql-server-ver15#guidelines-for-scripting-variable-names-and-values
* @param name variable name to validate
* @returns null if valid, otherwise an error message describing why input is invalid
*/
export function validateSqlCmdVariableName(name: string | undefined): string | null {
// remove $() around named if it's there
const cleanedName = removeSqlCmdVariableFormatting(name);
// can't contain whitespace
if (!cleanedName || cleanedName.trim() === '' || cleanedName.includes(' ')) {
return constants.sqlcmdVariableNameCannotContainWhitespace(name ?? '');
}
// can't contain these characters
if (constants.illegalSqlCmdChars.some(c => cleanedName?.includes(c))) {
return constants.sqlcmdVariableNameCannotContainIllegalChars(name ?? '');
}
// TODO: tsql parsing to check if it's a reserved keyword or invalid tsql https://github.com/microsoft/azuredatastudio/issues/12204
return null;
}
/**
* Recursively gets all the sqlproj files at any depth in a folder
* @param folderPath
*/
export async function getSqlProjectFilesInFolder(folderPath: string): Promise<string[]> {
// path needs to use forward slashes for glob to work
const escapedPath = glob.escapePath(folderPath.replace(/\\/g, '/'));
const sqlprojFilter = path.posix.join(escapedPath, '**', '*.sqlproj');
const results = await glob(sqlprojFilter);
return results;
}
/**
* Get all the projects in the workspace that are sqlproj
*/
export function getSqlProjectsInWorkspace(): Promise<vscode.Uri[]> {
const api = getDataWorkspaceExtensionApi();
return api.getProjectsInWorkspace(constants.sqlprojExtension);
}
export function getDataWorkspaceExtensionApi(): dataworkspace.IExtension {
const dataworkspaceExtName = getAzdataApi() ? dataworkspace.extension.name : dataworkspace.extension.vscodeName;
const extension = vscode.extensions.getExtension(dataworkspaceExtName)!;
return extension.exports;
}
export type IDacFxService = mssql.IDacFxService | vscodeMssql.IDacFxService;
export type ISchemaCompareService = mssql.ISchemaCompareService | vscodeMssql.ISchemaCompareService;
export type ISqlProjectsService = mssql.ISqlProjectsService | vscodeMssql.ISqlProjectsService;
export async function getDacFxService(): Promise<IDacFxService> {
if (getAzdataApi()) {
const ext = vscode.extensions.getExtension(mssql.extension.name) as vscode.Extension<mssql.IExtension>;
const api = await ext.activate();
return api.dacFx;
} else {
const api = await getVscodeMssqlApi();
return api.dacFx;
}
}
export async function getSchemaCompareService(): Promise<ISchemaCompareService> {
if (getAzdataApi()) {
const ext = vscode.extensions.getExtension(mssql.extension.name) as vscode.Extension<mssql.IExtension>;
const api = await ext.activate();
return api.schemaCompare;
} else {
const api = await getVscodeMssqlApi();
return api.schemaCompare;
}
}
export async function getSqlProjectsService(): Promise<ISqlProjectsService> {
if (getAzdataApi()) {
const ext = vscode.extensions.getExtension(mssql.extension.name) as vscode.Extension<mssql.IExtension>;
const api = await ext.activate();
return api.sqlProjects;
} else {
const api = await getVscodeMssqlApi();
return api.sqlProjects;
}
}
export async function getVscodeMssqlApi(): Promise<vscodeMssql.IExtension> {
const ext = vscode.extensions.getExtension(vscodeMssql.extension.name) as vscode.Extension<vscodeMssql.IExtension>;
return ext.activate();
}
export type AzureResourceServiceFactory = () => Promise<vscodeMssql.IAzureResourceService>;
export async function defaultAzureResourceServiceFactory(): Promise<vscodeMssql.IAzureResourceService> {
const vscodeMssqlApi = await getVscodeMssqlApi();
return vscodeMssqlApi.azureResourceService;
}
export type AzureAccountServiceFactory = () => Promise<vscodeMssql.IAzureAccountService>;
export async function defaultAzureAccountServiceFactory(): Promise<vscodeMssql.IAzureAccountService> {
const vscodeMssqlApi = await getVscodeMssqlApi();
return vscodeMssqlApi.azureAccountService;
}
/*
* Returns the default deployment options from DacFx, filtered to appropriate options for the given project.
*/
export async function getDefaultPublishDeploymentOptions(project: ISqlProject): Promise<mssql.DeploymentOptions | vscodeMssql.DeploymentOptions> {
const schemaCompareService = await getSchemaCompareService();
const result = await schemaCompareService.schemaCompareGetDefaultOptions();
// this option needs to be true for same database references validation to work
if (project.databaseReferences.length > 0) {
result.defaultDeploymentOptions.booleanOptionsDictionary.includeCompositeObjects.value = true;
}
return result.defaultDeploymentOptions;
}
export interface IPackageInfo {
name: string;
version: string;
aiKey: string;
}
export function getPackageInfo(packageJson?: any): IPackageInfo | undefined {
if (!packageJson) {
packageJson = require('../../package.json');
}
const vscodePackageJson = require('../../package.vscode.json');
const azdataApi = getAzdataApi();
if (!packageJson || !azdataApi && !vscodePackageJson) {
return undefined;
}
// When the extension is compiled and packaged, the content of package.json get copied here in the extension.js. This happens before the
// package.vscode.json values replace the corresponding values in the package.json for the sql-database-projects-vscode extension
// so we need to read these values directly from the package.vscode.json to get the correct extension and publisher names
const extensionName = azdataApi ? packageJson.name : vscodePackageJson.name;
return {
name: extensionName,
version: packageJson.version,
aiKey: packageJson.aiKey
};
}
/**
* Converts time in milliseconds to hr, min, sec
* @param duration time in milliseconds
* @returns string in "hr, min, sec" or "msec" format
*/
export function timeConversion(duration: number): string {
const portions: string[] = [];
const msInHour = 1000 * 60 * 60;
const hours = Math.trunc(duration / msInHour);
if (hours > 0) {
portions.push(`${hours} ${constants.hr}`);
duration = duration - (hours * msInHour);
}
const msInMinute = 1000 * 60;
const minutes = Math.trunc(duration / msInMinute);
if (minutes > 0) {
portions.push(`${minutes} ${constants.min}`);
duration = duration - (minutes * msInMinute);
}
const seconds = Math.trunc(duration / 1000);
if (seconds > 0) {
portions.push(`${seconds} ${constants.sec}`);
}
if (hours === 0 && minutes === 0 && seconds === 0) {
portions.push(`${duration} ${constants.msec}`);
}
return portions.join(', ');
}
// Try to load the azdata API - but gracefully handle the failure in case we're running
// in a context where the API doesn't exist (such as VS Code)
let azdataApi: typeof azdataType | undefined = undefined;
try {
azdataApi = require('azdata');
if (!azdataApi?.version) {
// webpacking makes the require return an empty object instead of throwing an error so make sure we clear the var
azdataApi = undefined;
}
} catch {
// no-op
}
/**
* Gets the azdata API if it's available in the context this extension is running in.
* @returns The azdata API if it's available
*/
export function getAzdataApi(): typeof azdataType | undefined {
return azdataApi;
}
export async function createFolderIfNotExist(folderPath: string): Promise<void> {
try {
await fse.mkdir(folderPath);
} catch {
// Ignore if failed
}
}
export async function retry<T>(
name: string,
attempt: () => Promise<T>,
verify: (result: T) => Promise<ValidationResult>,
formatResult: (result: T) => Promise<string>,
outputChannel: vscode.OutputChannel,
numberOfAttempts: number = 10,
waitInSeconds: number = 2
): Promise<T | undefined> {
for (let count = 0; count < numberOfAttempts; count++) {
outputChannel.appendLine(constants.retryWaitMessage(waitInSeconds, name));
await new Promise(c => setTimeout(c, waitInSeconds * 1000));
outputChannel.appendLine(constants.retryRunMessage(count, numberOfAttempts, name));
try {
let result = await attempt();
const validationResult = await verify(result);
const formattedResult = await formatResult(result);
if (validationResult.validated) {
outputChannel.appendLine(constants.retrySucceedMessage(name, formattedResult));
return result;
} else {
outputChannel.appendLine(constants.retryFailedMessage(name, formattedResult, validationResult.errorMessage));
}
} catch (err) {
outputChannel.appendLine(constants.retryMessage(name, getErrorMessage(err)));
}
}
return undefined;
}
/**
* Detects whether the specified command-line command is available on the current machine
*/
export async function detectCommandInstallation(command: string): Promise<boolean> {
try {
const found = await which(command);
if (found) {
return true;
}
} catch (err) {
console.log(getErrorMessage(err));
}
return false;
}
export function validateSqlServerPortNumber(port: string | undefined): boolean {
if (!port) {
return false;
}
const valueAsNum = +port;
return !isNaN(valueAsNum) && valueAsNum > 0 && valueAsNum < 65535;
}
export function isEmptyString(input: string | undefined): boolean {
return input === undefined || input === '';
}
export function isValidSQLPassword(password: string, userName: string = 'sa'): boolean {
// Validate SQL Server password
const containsUserName = password && userName !== undefined && password.toUpperCase().includes(userName.toUpperCase());
// Instead of using one RegEx, I am separating it to make it more readable.
const hasUpperCase = /[A-Z]/.test(password) ? 1 : 0;
const hasLowerCase = /[a-z]/.test(password) ? 1 : 0;
const hasNumbers = /\d/.test(password) ? 1 : 0;
const hasNonAlphas = /\W/.test(password) ? 1 : 0;
return !containsUserName && password.length >= 8 && password.length <= 128 && (hasUpperCase + hasLowerCase + hasNumbers + hasNonAlphas >= 3);
}
export async function showErrorMessageWithOutputChannel(errorMessageFunc: (error: string) => string, error: any, outputChannel: vscode.OutputChannel): Promise<void> {
const result = await vscode.window.showErrorMessage(errorMessageFunc(getErrorMessage(error)), constants.checkoutOutputMessage);
if (result === constants.checkoutOutputMessage) {
outputChannel.show();
}
}
export async function showInfoMessageWithOutputChannel(message: string, outputChannel: vscode.OutputChannel): Promise<void> {
const result = await vscode.window.showInformationMessage(message, constants.checkoutOutputMessage);
if (result === constants.checkoutOutputMessage) {
outputChannel.show();
}
}
/**
* Returns the results of the glob pattern
* @param pattern Glob pattern to search for
*/
export async function globWithPattern(pattern: string): Promise<string[]> {
const forwardSlashPattern = pattern.replace(/\\/g, '/');
return await glob(forwardSlashPattern);
}
/**
* Recursively gets all the sql files at any depth in a folder
* @param folderPath
* @param ignoreBinObj ignore sql files in bin and obj folders
*/
export async function getSqlFilesInFolder(folderPath: string, ignoreBinObj?: boolean): Promise<string[]> {
// path needs to use forward slashes for glob to work
folderPath = folderPath.replace(/\\/g, '/');
const sqlFilter = path.posix.join(folderPath, '**', '*.sql');
if (ignoreBinObj) {
// don't add files in bin and obj folders
const binIgnore = path.posix.join(folderPath, 'bin', '**', '*.sql');
const objIgnore = path.posix.join(folderPath, 'obj', '**', '*.sql');
return await glob(sqlFilter, { ignore: [binIgnore, objIgnore] });
} else {
return await glob(sqlFilter);
}
}
/**
* Recursively gets all the folders at any depth in the given folder
* @param folderPath
* @param ignoreBinObj ignore bin and obj folders
*/
export async function getFoldersInFolder(folderPath: string, ignoreBinObj?: boolean): Promise<string[]> {
// path needs to use forward slashes for glob to work
const escapedPath = glob.escapePath(folderPath.replace(/\\/g, '/'));
const folderFilter = path.posix.join(escapedPath, '/**');
if (ignoreBinObj) {
// don't add bin and obj folders
const binIgnore = path.posix.join(escapedPath, 'bin');
const objIgnore = path.posix.join(escapedPath, 'obj');
return await glob(folderFilter, { onlyDirectories: true, ignore: [binIgnore, objIgnore] });
} else {
return await glob(folderFilter, { onlyDirectories: true });
}
}
/**
* Gets the folders between the startFolder to the file
* @param startFolder
* @param endFile
* @returns array of folders between startFolder and endFile
*/
export function getFoldersToFile(startFolder: string, endFile: string): string[] {
let folders: string[] = [];
const endFolderPath = path.dirname(endFile);
const relativePath = convertSlashesForSqlProj(endFolderPath.substring(startFolder.length));
const pathSegments = trimChars(relativePath, ' \\').split(constants.SqlProjPathSeparator);
let folderPath = convertSlashesForSqlProj(startFolder) + constants.SqlProjPathSeparator;
for (let segment of pathSegments) {
if (segment) {
folderPath += segment + constants.SqlProjPathSeparator;
folders.push(getPlatformSafeFileEntryPath(folderPath));
}
}
return folders;
}
/**
* Gets the folders between the startFolder and endFolder
* @param startFolder
* @param endFolder
* @returns array of folders between startFolder and endFolder
*/
export function getFoldersAlongPath(startFolder: string, endFolder: string): string[] {
let folders: string[] = [];
const relativePath = convertSlashesForSqlProj(endFolder.substring(startFolder.length));
const pathSegments = trimChars(relativePath, ' \\').split(constants.SqlProjPathSeparator);
let folderPath = convertSlashesForSqlProj(startFolder) + constants.SqlProjPathSeparator;
for (let segment of pathSegments) {
if (segment) {
folderPath += segment + constants.SqlProjPathSeparator;
folders.push(getPlatformSafeFileEntryPath(folderPath));
}
}
return folders;
}
/**
* Returns SQL version number from docker image name which is in the beginning of the image name
* @param imageName docker image name
* @returns SQL server version
*/
export function findSqlVersionInImageName(imageName: string): number | undefined {
// Regex to find the version in the beginning of the image name
// e.g. 2017-CU16-ubuntu, 2019-latest
const regex = new RegExp('^([0-9]+)[-].+$');
if (regex.test(imageName)) {
const finds = regex.exec(imageName);
if (finds) {
// 0 is the full match and 1 is the number with pattern inside the first ()
return +finds[1];
}
}
return undefined;
}
/**
* Returns SQL version number from target platform name
* @param targetPlatform target platform
* @returns SQL server version
*/
export function findSqlVersionInTargetPlatform(targetPlatform: string): number | undefined {
// Regex to find the version in target platform
// e.g. SQL Server 2019
const regex = new RegExp('([0-9]+)$');
if (regex.test(targetPlatform)) {
const finds = regex.exec(targetPlatform);
if (finds) {
// 0 is the full match and 1 is the number with pattern inside the first ()
return +finds[1];
}
}
return undefined;
}
export function throwIfNotConnected(connectionResult: azdataType.ConnectionResult): void {
if (!connectionResult.connected) {
throw new Error(`${connectionResult.errorMessage} (${connectionResult.errorCode})`);
}
}
/**
* Checks whether or not the provided file contains a create table statement
* @param fullPath full path to file to check
* @param projectTargetVersion target version of sql project containing this file
* @returns true if file includes a create table statement, false if it doesn't
*/
export async function fileContainsCreateTableStatement(fullPath: string, projectTargetVersion: string): Promise<boolean> {
let containsCreateTableStatement = false;
if (getAzdataApi() && await exists(fullPath)) {
const dacFxService = await getDacFxService() as mssql.IDacFxService;
try {
const result = await dacFxService.parseTSqlScript(fullPath, projectTargetVersion);
containsCreateTableStatement = result.containsCreateTableStatement;
} catch (e) {
console.error(getErrorMessage(e));
}
}
return containsCreateTableStatement;
}
/**
* Gets target platform based on the server edition/version
* @param serverInfo server information
* @param serverUrl optional server URL, only used to check if it's a known domain for Microsoft Fabric DW
* @returns target platform for the database project
*/
export async function getTargetPlatformFromServerVersion(serverInfo: azdataType.ServerInfo | vscodeMssql.IServerInfo, serverUrl?: string): Promise<SqlTargetPlatform | undefined> {
const isCloud = serverInfo.isCloud;
let targetPlatform;
if (isCloud) {
const engineEdition = serverInfo.engineEditionId;
const azdataApi = getAzdataApi();
if (azdataApi) {
// TODO: Update this when Fabric DW gets its own engine edition
// https://github.com/microsoft/azuredatastudio/issues/24112
if (engineEdition === azdataApi.DatabaseEngineEdition.SqlOnDemand) {
targetPlatform = isSqlDwUnifiedServer(serverUrl) ? SqlTargetPlatform.sqlDwUnified : SqlTargetPlatform.sqlDwServerless;
} else if (engineEdition === azdataApi.DatabaseEngineEdition.SqlDataWarehouse) {
targetPlatform = SqlTargetPlatform.sqlDW;
} else {
targetPlatform = SqlTargetPlatform.sqlAzure;
}
} else {
// TODO: Update this when Fabric DW gets its own engine edition
// https://github.com/microsoft/azuredatastudio/issues/24112
if (engineEdition === vscodeMssql.DatabaseEngineEdition.SqlOnDemand) {
targetPlatform = isSqlDwUnifiedServer(serverUrl) ? SqlTargetPlatform.sqlDwUnified : SqlTargetPlatform.sqlDwServerless;
} else if (engineEdition === vscodeMssql.DatabaseEngineEdition.SqlDataWarehouse) {
targetPlatform = SqlTargetPlatform.sqlDW;
} else {
targetPlatform = SqlTargetPlatform.sqlAzure;
}
}
} else {
const serverMajorVersion = serverInfo.serverMajorVersion;
targetPlatform = serverMajorVersion ? constants.onPremServerVersionToTargetPlatform.get(serverMajorVersion) : undefined;
}
return targetPlatform;
}
/**
* Determines if a server name is a known domain for Microsoft Fabric DW. This is required because the engine edition for Fabric DW is the same as Serverless.
* @param server The server name to check
* @returns True if the server name matches a known domain for Microsoft Fabric DW, otherwise false
*/
export function isSqlDwUnifiedServer(server?: string): boolean | undefined {
const serverLowerCase = server?.toLowerCase();
return serverLowerCase?.includes("datawarehouse.pbidedicated.windows.net") || serverLowerCase?.includes("datawarehouse.fabric.microsoft.com");
}
/**
* Determines if a given character is a valid filename character
* @param c Character to validate
*/
export function isValidFilenameCharacter(c: string): boolean {
return getDataWorkspaceExtensionApi().isValidFilenameCharacter(c);
}
/**
* Replaces invalid filename characters in a string with underscores
* @param s The string to be sanitized for a filename
*/
export function sanitizeStringForFilename(s: string): string {
return getDataWorkspaceExtensionApi().sanitizeStringForFilename(s);
}
/**
* Returns true if the string is a valid filename
* @param name filename to check
*/
export function isValidBasename(name?: string): boolean {
return getDataWorkspaceExtensionApi().isValidBasename(name);
}
/**
* Returns specific error message if file name is invalid
* @param name filename to check
*/
export function isValidBasenameErrorMessage(name?: string): string | undefined {
return getDataWorkspaceExtensionApi().isValidBasenameErrorMessage(name);
}
/**
* Checks if the provided file is a publish profile
* @param fileName filename to check
* @returns True if it is a publish profile, otherwise false
*/
export function isPublishProfile(fileName: string): boolean {
const hasPublishExtension = fileName.trim().toLowerCase().endsWith(constants.publishProfileExtension);
return hasPublishExtension;
}
/**
* Checks to see if a file exists at absoluteFilePath, and writes contents if it doesn't.
* If either the file already exists and contents is specified or the file doesn't exist and contents is blank,
* then an exception is thrown.
* @param absoluteFilePath
* @param contents
*/
export async function ensureFileExists(absoluteFilePath: string, contents?: string): Promise<void> {
if (contents) {
// Create the file if contents were passed in and file does not exist yet
await fs.mkdir(path.dirname(absoluteFilePath), { recursive: true });
try {
await fs.writeFile(absoluteFilePath, contents, { flag: 'wx' });
} catch (error) {
if (error.code === 'EEXIST') {
// Throw specialized error, if file already exists
throw new Error(constants.fileAlreadyExists(path.parse(absoluteFilePath).name));
}
throw error;
}
} else {
// If no contents were provided, then check that file already exists
if (!await exists(absoluteFilePath)) {
throw new Error(constants.noFileExist(absoluteFilePath));
}
}
}
export function throwIfFailed(result: azdataType.ResultStatus | vscodeMssql.ResultStatus): void {
if (!result.success) {
throw new Error(constants.errorPrefix(result.errorMessage));
}
}