@zapier/zapier-sdk 0.13.0 → 0.13.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/CHANGELOG.md +12 -0
- package/README.md +49 -10
- package/dist/index.cjs +39 -20
- package/dist/index.d.mts +22 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.mjs +38 -21
- package/dist/plugins/getApp/index.test.js +58 -6
- package/dist/plugins/listApps/index.d.ts.map +1 -1
- package/dist/plugins/listApps/index.js +18 -10
- package/dist/schemas/App.d.ts.map +1 -1
- package/dist/schemas/App.js +11 -1
- package/dist/utils/domain-utils.d.ts.map +1 -1
- package/dist/utils/domain-utils.js +2 -6
- package/dist/utils/string-utils.d.ts +9 -0
- package/dist/utils/string-utils.d.ts.map +1 -1
- package/dist/utils/string-utils.js +25 -0
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/plugins/getApp/index.test.ts +83 -31
- package/src/plugins/listApps/index.ts +19 -10
- package/src/schemas/App.ts +12 -1
- package/src/utils/domain-utils.ts +2 -6
- package/src/utils/string-utils.ts +28 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain-utils.d.ts","sourceRoot":"","sources":["../../src/utils/domain-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE/E;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,GACnB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAQ9B;AAED;;;;;GAKG;AACH,wBAAgB,oCAAoC,CAClD,kBAAkB,EAAE,kBAAkB,GACrC,OAAO,
|
|
1
|
+
{"version":3,"file":"domain-utils.d.ts","sourceRoot":"","sources":["../../src/utils/domain-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE/E;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,GACnB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAQ9B;AAED;;;;;GAKG;AACH,wBAAgB,oCAAoC,CAClD,kBAAkB,EAAE,kBAAkB,GACrC,OAAO,CAYT;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,cAAc,EACpB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GACvD,kBAAkB,CAqCpB;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAsB9D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG;IAC9D,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAgCA;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG;IACrD,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAUA;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOtD;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAY1D;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAI9C;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAkBvD;AAED,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,UAAU,GACrB,UAAU,IAAI,kBAAkB,CAElC;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,kBAAkB,GAAG,MAAM,CAEzE"}
|
|
@@ -27,18 +27,14 @@ export function splitVersionedKey(versionedKey) {
|
|
|
27
27
|
* @returns Normalized AppItem with essential fields only
|
|
28
28
|
*/
|
|
29
29
|
export function normalizeImplementationMetaToAppItem(implementationMeta) {
|
|
30
|
-
// Extract API name and version from the implementation ID
|
|
31
30
|
const [selectedApi, appVersion] = splitVersionedKey(implementationMeta.id);
|
|
32
|
-
// Destructure to exclude id and name before spreading
|
|
33
31
|
const { id, name, ...restOfImplementationMeta } = implementationMeta;
|
|
34
32
|
return {
|
|
35
|
-
// Pass through all ImplementationMeta fields except id and name
|
|
36
33
|
...restOfImplementationMeta,
|
|
37
|
-
// Transform key fields
|
|
38
34
|
title: name,
|
|
39
35
|
key: selectedApi,
|
|
40
|
-
implementation_id: id,
|
|
41
|
-
version: appVersion,
|
|
36
|
+
implementation_id: id,
|
|
37
|
+
version: appVersion,
|
|
42
38
|
};
|
|
43
39
|
}
|
|
44
40
|
/**
|
|
@@ -9,4 +9,13 @@
|
|
|
9
9
|
* - mixed formats: "first_name-value" → "First Name Value"
|
|
10
10
|
*/
|
|
11
11
|
export declare function toTitleCase(input: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Converts a string to snake_case, handling various input formats:
|
|
14
|
+
* - camelCase: "firstName" → "first_name"
|
|
15
|
+
* - kebab-case: "first-name" → "first_name"
|
|
16
|
+
* - title case: "First Name" → "first_name"
|
|
17
|
+
* - mixed formats: "first-Name Value" → "first_name_value"
|
|
18
|
+
* - starts with number: "123abc" → "_123abc"
|
|
19
|
+
*/
|
|
20
|
+
export declare function toSnakeCase(input: string): string;
|
|
12
21
|
//# sourceMappingURL=string-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"string-utils.d.ts","sourceRoot":"","sources":["../../src/utils/string-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAcjD"}
|
|
1
|
+
{"version":3,"file":"string-utils.d.ts","sourceRoot":"","sources":["../../src/utils/string-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAcjD;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAkBjD"}
|
|
@@ -21,3 +21,28 @@ export function toTitleCase(input) {
|
|
|
21
21
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
22
22
|
.join(" "));
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Converts a string to snake_case, handling various input formats:
|
|
26
|
+
* - camelCase: "firstName" → "first_name"
|
|
27
|
+
* - kebab-case: "first-name" → "first_name"
|
|
28
|
+
* - title case: "First Name" → "first_name"
|
|
29
|
+
* - mixed formats: "first-Name Value" → "first_name_value"
|
|
30
|
+
* - starts with number: "123abc" → "_123abc"
|
|
31
|
+
*/
|
|
32
|
+
export function toSnakeCase(input) {
|
|
33
|
+
let result = input
|
|
34
|
+
// insert underscore before capital letters (handles camelCase)
|
|
35
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
36
|
+
// replace spaces and dashes with underscores
|
|
37
|
+
.replace(/[\s\-]+/g, "_")
|
|
38
|
+
// replace multiple underscores with single underscore
|
|
39
|
+
.replace(/_+/g, "_")
|
|
40
|
+
// remove leading/trailing underscores and convert to lowercase
|
|
41
|
+
.replace(/^_|_$/g, "")
|
|
42
|
+
.toLowerCase();
|
|
43
|
+
// If the result starts with a number, prefix with underscore
|
|
44
|
+
if (/^[0-9]/.test(result)) {
|
|
45
|
+
result = "_" + result;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -42,6 +42,9 @@ export { isPositional, PositionalMetadata } from "./utils/schema-utils";
|
|
|
42
42
|
export { createFunction } from "./utils/function-utils";
|
|
43
43
|
export type { FormattedItem, FormatMetadata } from "./utils/schema-utils";
|
|
44
44
|
|
|
45
|
+
// Export string utilities
|
|
46
|
+
export { toSnakeCase, toTitleCase } from "./utils/string-utils";
|
|
47
|
+
|
|
45
48
|
// Export resolver utilities for CLI
|
|
46
49
|
export * from "./resolvers";
|
|
47
50
|
|
|
@@ -52,19 +52,41 @@ describe("getApp plugin", () => {
|
|
|
52
52
|
const sdk = createTestSdk();
|
|
53
53
|
const context = sdk.getContext();
|
|
54
54
|
|
|
55
|
-
// Mock the API client
|
|
56
|
-
(context.api as any).get = vi
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
// Mock the API client to handle both resolveAppKeys and listApps calls
|
|
56
|
+
(context.api as any).get = vi
|
|
57
|
+
.fn()
|
|
58
|
+
// First call: resolveAppKeys may call to resolve slug to implementation name
|
|
59
|
+
.mockResolvedValueOnce({
|
|
60
|
+
results: [
|
|
61
|
+
{
|
|
62
|
+
id: "SlackCLIAPI@1.0.0",
|
|
63
|
+
name: "Slack",
|
|
64
|
+
description: "Team communication",
|
|
65
|
+
primary_color: "#4A154B",
|
|
66
|
+
categories: ["communication"],
|
|
67
|
+
slug: "slack",
|
|
68
|
+
key: "SlackCLIAPI",
|
|
69
|
+
version: "1.0.0",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
next: null,
|
|
73
|
+
})
|
|
74
|
+
// Second call: listApps main API call
|
|
75
|
+
.mockResolvedValueOnce({
|
|
76
|
+
results: [
|
|
77
|
+
{
|
|
78
|
+
id: "SlackCLIAPI@1.0.0",
|
|
79
|
+
name: "Slack",
|
|
80
|
+
description: "Team communication",
|
|
81
|
+
primary_color: "#4A154B",
|
|
82
|
+
categories: ["communication"],
|
|
83
|
+
slug: "slack",
|
|
84
|
+
key: "SlackCLIAPI",
|
|
85
|
+
version: "1.0.0",
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
next: null,
|
|
89
|
+
});
|
|
68
90
|
|
|
69
91
|
const result = await sdk.getApp({
|
|
70
92
|
appKey: "slack",
|
|
@@ -82,11 +104,19 @@ describe("getApp plugin", () => {
|
|
|
82
104
|
const sdk = createTestSdk();
|
|
83
105
|
const context = sdk.getContext();
|
|
84
106
|
|
|
85
|
-
// Mock API to return empty results
|
|
86
|
-
(context.api as any).get = vi
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
107
|
+
// Mock API to return empty results for both potential calls
|
|
108
|
+
(context.api as any).get = vi
|
|
109
|
+
.fn()
|
|
110
|
+
// First call: resolveAppKeys may call to resolve slug
|
|
111
|
+
.mockResolvedValueOnce({
|
|
112
|
+
results: [],
|
|
113
|
+
next: null,
|
|
114
|
+
})
|
|
115
|
+
// Second call: listApps main API call
|
|
116
|
+
.mockResolvedValueOnce({
|
|
117
|
+
results: [],
|
|
118
|
+
next: null,
|
|
119
|
+
});
|
|
90
120
|
|
|
91
121
|
await expect(
|
|
92
122
|
sdk.getApp({
|
|
@@ -101,19 +131,41 @@ describe("getApp plugin", () => {
|
|
|
101
131
|
const sdk = createTestSdk();
|
|
102
132
|
const context = sdk.getContext();
|
|
103
133
|
|
|
104
|
-
// Mock the API client
|
|
105
|
-
(context.api as any).get = vi
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
// Mock the API client to handle both resolveAppKeys and listApps calls
|
|
135
|
+
(context.api as any).get = vi
|
|
136
|
+
.fn()
|
|
137
|
+
// First call: resolveAppKeys may call to resolve slug to implementation name
|
|
138
|
+
.mockResolvedValueOnce({
|
|
139
|
+
results: [
|
|
140
|
+
{
|
|
141
|
+
id: "TestCLIAPI@1.0.0",
|
|
142
|
+
name: "Test App",
|
|
143
|
+
description: "Test description",
|
|
144
|
+
primary_color: "#FF0000",
|
|
145
|
+
categories: ["testing"],
|
|
146
|
+
slug: "test",
|
|
147
|
+
key: "TestCLIAPI",
|
|
148
|
+
version: "1.0.0",
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
next: null,
|
|
152
|
+
})
|
|
153
|
+
// Second call: listApps main API call
|
|
154
|
+
.mockResolvedValueOnce({
|
|
155
|
+
results: [
|
|
156
|
+
{
|
|
157
|
+
id: "TestCLIAPI@1.0.0",
|
|
158
|
+
name: "Test App",
|
|
159
|
+
description: "Test description",
|
|
160
|
+
primary_color: "#FF0000",
|
|
161
|
+
categories: ["testing"],
|
|
162
|
+
slug: "test",
|
|
163
|
+
key: "TestCLIAPI",
|
|
164
|
+
version: "1.0.0",
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
next: null,
|
|
168
|
+
});
|
|
117
169
|
|
|
118
170
|
const result = await sdk.getApp({
|
|
119
171
|
appKey: "test",
|
|
@@ -41,11 +41,11 @@ export const listAppsPlugin: Plugin<
|
|
|
41
41
|
const listApps = createPaginatedFunction(async function listAppsPage(
|
|
42
42
|
options: ListAppsOptions & { cursor?: string } & { pageSize: number },
|
|
43
43
|
): Promise<ListAppsPage> {
|
|
44
|
-
const api = context
|
|
45
|
-
const
|
|
44
|
+
const { api, resolveAppKeys } = context;
|
|
45
|
+
const appKeys = options.appKeys ?? [];
|
|
46
46
|
|
|
47
|
-
const appLocators = await
|
|
48
|
-
appKeys: [...
|
|
47
|
+
const appLocators = await resolveAppKeys({
|
|
48
|
+
appKeys: [...appKeys],
|
|
49
49
|
});
|
|
50
50
|
const implementationNameToLocator: Record<string, ResolvedAppLocator[]> =
|
|
51
51
|
{};
|
|
@@ -67,10 +67,10 @@ export const listAppsPlugin: Plugin<
|
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
if (
|
|
70
|
+
if (options.search) {
|
|
71
71
|
const searchParams: Record<string, string> = {};
|
|
72
72
|
|
|
73
|
-
searchParams.term =
|
|
73
|
+
searchParams.term = options.search;
|
|
74
74
|
|
|
75
75
|
const searchEnvelope: ImplementationsMetaResponse = await api.get(
|
|
76
76
|
"/api/v4/implementations-meta/search/",
|
|
@@ -101,22 +101,31 @@ export const listAppsPlugin: Plugin<
|
|
|
101
101
|
|
|
102
102
|
const searchParams: Record<string, string> = {};
|
|
103
103
|
|
|
104
|
-
if (
|
|
105
|
-
searchParams.limit =
|
|
104
|
+
if (options.pageSize) {
|
|
105
|
+
searchParams.limit = options.pageSize.toString();
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
if (appLocators.length === 0) {
|
|
109
109
|
searchParams.latest_only = "true";
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
if (
|
|
113
|
-
searchParams.offset =
|
|
112
|
+
if (options.cursor) {
|
|
113
|
+
searchParams.offset = options.cursor;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
searchParams.selected_apis = appLocators
|
|
117
117
|
.map((locator) => toImplementationId(locator))
|
|
118
118
|
.join(",");
|
|
119
119
|
|
|
120
|
+
// If we have app keys, but they resolved to an empty list, then we need to short-circuit and return an empty list.
|
|
121
|
+
// Otherwise, the API will return all apps, since no selected_apis are provided.
|
|
122
|
+
if (appKeys.length > 0 && appLocators.length === 0) {
|
|
123
|
+
return {
|
|
124
|
+
data: [],
|
|
125
|
+
nextCursor: undefined,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
120
129
|
const implementationsEnvelope: ImplementationsMetaResponse = await api.get(
|
|
121
130
|
"/api/v4/implementations-meta/lookup/",
|
|
122
131
|
{
|
package/src/schemas/App.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { withFormatter } from "../utils/schema-utils";
|
|
3
3
|
import { ImplementationMetaSchema } from "../api/schemas";
|
|
4
|
+
import { toSnakeCase } from "../utils/string-utils";
|
|
4
5
|
|
|
5
6
|
export { FormattedItem, FormatMetadata } from "../utils/schema-utils";
|
|
6
7
|
|
|
@@ -16,10 +17,20 @@ export const AppItemSchema = withFormatter(
|
|
|
16
17
|
}),
|
|
17
18
|
{
|
|
18
19
|
format: (item) => {
|
|
20
|
+
// Create additional keys if slug exists
|
|
21
|
+
const additionalKeys = [];
|
|
22
|
+
if (item.slug && item.slug !== item.key) {
|
|
23
|
+
additionalKeys.push(item.slug);
|
|
24
|
+
const snakeCaseSlug = toSnakeCase(item.slug);
|
|
25
|
+
if (snakeCaseSlug !== item.slug && snakeCaseSlug !== item.key) {
|
|
26
|
+
additionalKeys.push(snakeCaseSlug);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
return {
|
|
20
31
|
title: item.title,
|
|
21
32
|
key: item.key,
|
|
22
|
-
keys: [item.
|
|
33
|
+
keys: [item.key, ...additionalKeys],
|
|
23
34
|
description: item.description,
|
|
24
35
|
details: [],
|
|
25
36
|
};
|
|
@@ -36,20 +36,16 @@ export function splitVersionedKey(
|
|
|
36
36
|
export function normalizeImplementationMetaToAppItem(
|
|
37
37
|
implementationMeta: ImplementationMeta,
|
|
38
38
|
): AppItem {
|
|
39
|
-
// Extract API name and version from the implementation ID
|
|
40
39
|
const [selectedApi, appVersion] = splitVersionedKey(implementationMeta.id);
|
|
41
40
|
|
|
42
|
-
// Destructure to exclude id and name before spreading
|
|
43
41
|
const { id, name, ...restOfImplementationMeta } = implementationMeta;
|
|
44
42
|
|
|
45
43
|
return {
|
|
46
|
-
// Pass through all ImplementationMeta fields except id and name
|
|
47
44
|
...restOfImplementationMeta,
|
|
48
|
-
// Transform key fields
|
|
49
45
|
title: name,
|
|
50
46
|
key: selectedApi,
|
|
51
|
-
implementation_id: id,
|
|
52
|
-
version: appVersion,
|
|
47
|
+
implementation_id: id,
|
|
48
|
+
version: appVersion,
|
|
53
49
|
};
|
|
54
50
|
}
|
|
55
51
|
|
|
@@ -24,3 +24,31 @@ export function toTitleCase(input: string): string {
|
|
|
24
24
|
.join(" ")
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Converts a string to snake_case, handling various input formats:
|
|
30
|
+
* - camelCase: "firstName" → "first_name"
|
|
31
|
+
* - kebab-case: "first-name" → "first_name"
|
|
32
|
+
* - title case: "First Name" → "first_name"
|
|
33
|
+
* - mixed formats: "first-Name Value" → "first_name_value"
|
|
34
|
+
* - starts with number: "123abc" → "_123abc"
|
|
35
|
+
*/
|
|
36
|
+
export function toSnakeCase(input: string): string {
|
|
37
|
+
let result = input
|
|
38
|
+
// insert underscore before capital letters (handles camelCase)
|
|
39
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
40
|
+
// replace spaces and dashes with underscores
|
|
41
|
+
.replace(/[\s\-]+/g, "_")
|
|
42
|
+
// replace multiple underscores with single underscore
|
|
43
|
+
.replace(/_+/g, "_")
|
|
44
|
+
// remove leading/trailing underscores and convert to lowercase
|
|
45
|
+
.replace(/^_|_$/g, "")
|
|
46
|
+
.toLowerCase();
|
|
47
|
+
|
|
48
|
+
// If the result starts with a number, prefix with underscore
|
|
49
|
+
if (/^[0-9]/.test(result)) {
|
|
50
|
+
result = "_" + result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return result;
|
|
54
|
+
}
|