Skip to content

Commit 2d3e7c0

Browse files
authored
Merge pull request #1103 from lowcoder-org/versions-for-datasources
Versions for API based Datasources
2 parents d63d3a0 + 1a50316 commit 2d3e7c0

File tree

85 files changed

+4026
-161
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+4026
-161
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { specsToOptions, version2spec } from "./util";
2+
3+
describe('version2spec', () => {
4+
test('should return the spec for the given version', () => {
5+
const specs = {
6+
v1: 'spec for version 1',
7+
v2: 'spec for version 2',
8+
v3: 'spec for version 3'
9+
};
10+
11+
expect(version2spec(specs, 'v2')).toBe('spec for version 2');
12+
});
13+
14+
test('should return the first spec if version is undefined', () => {
15+
const specs = {
16+
v1: 'spec for version 1',
17+
v2: 'spec for version 2',
18+
v3: 'spec for version 3'
19+
};
20+
21+
expect(version2spec(specs, undefined)).toBe('spec for version 1');
22+
});
23+
24+
test('should return the first spec if version is an empty string', () => {
25+
const specs = {
26+
v1: 'spec for version 1',
27+
v2: 'spec for version 2',
28+
v3: 'spec for version 3'
29+
};
30+
31+
expect(version2spec(specs, "")).toBe('spec for version 1');
32+
});
33+
34+
test('should return undefined if specs is an empty object and version is undefined', () => {
35+
const specs = {};
36+
37+
expect(version2spec(specs, undefined)).toBeUndefined();
38+
});
39+
40+
test('should return undefined if specs is an empty object and version is an empty string', () => {
41+
const specs = {};
42+
43+
expect(version2spec(specs, "")).toBeUndefined();
44+
});
45+
46+
test('should return undefined if the specified version does not exist in specs', () => {
47+
const specs = {
48+
v1: 'spec for version 1',
49+
v2: 'spec for version 2',
50+
v3: 'spec for version 3'
51+
};
52+
53+
expect(version2spec(specs, 'v4')).toBeUndefined();
54+
});
55+
});
56+
57+
describe('specsToOptions', () => {
58+
test('should convert specs object to options array', () => {
59+
const specs = {
60+
color: 'red',
61+
size: 'large',
62+
weight: 'light'
63+
};
64+
65+
const expectedOptions = [
66+
{ value: 'color', label: 'color' },
67+
{ value: 'size', label: 'size' },
68+
{ value: 'weight', label: 'weight' }
69+
];
70+
71+
expect(specsToOptions(specs)).toEqual(expectedOptions);
72+
});
73+
74+
test('should return an empty array if specs object is empty', () => {
75+
const specs = {};
76+
const expectedOptions: any[] = [];
77+
78+
expect(specsToOptions(specs)).toEqual(expectedOptions);
79+
});
80+
81+
test('should handle specs object with non-string values', () => {
82+
const specs = {
83+
color: 'red',
84+
size: 42,
85+
available: true
86+
};
87+
88+
const expectedOptions = [
89+
{ value: 'color', label: 'color' },
90+
{ value: 'size', label: 'size' },
91+
{ value: 'available', label: 'available' }
92+
];
93+
94+
expect(specsToOptions(specs)).toEqual(expectedOptions);
95+
});
96+
97+
test('should handle specs object with numeric keys', () => {
98+
const specs = {
99+
1: 'one',
100+
2: 'two',
101+
3: 'three'
102+
};
103+
104+
const expectedOptions = [
105+
{ value: '1', label: '1' },
106+
{ value: '2', label: '2' },
107+
{ value: '3', label: '3' }
108+
];
109+
110+
expect(specsToOptions(specs)).toEqual(expectedOptions);
111+
});
112+
});

server/node-service/src/common/util.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import yaml from "yaml";
22
import fs from "fs";
3+
import { MultiOpenApiSpecItem } from "../plugins/openApi/parse";
4+
import path from "path";
5+
import { appendTags } from "../plugins/openApi/util";
6+
import _ from "lodash";
37

48
export function kvToRecord(
59
kvs: { key: string; value: string }[],
@@ -85,3 +89,45 @@ export function safeJsonStringify(data: any) {
8589
return null;
8690
}
8791
}
92+
93+
export function specsToOptions(specs: any) {
94+
return Object.keys(specs).map(k => ({value: k, label: k}));
95+
}
96+
97+
export function version2spec(specs: any, version: any) {
98+
if(version == undefined || version == "") {
99+
const keys = Object.keys(specs);
100+
if(keys.length == 0) return;
101+
return specs[keys[0]];
102+
}
103+
return specs[version];
104+
}
105+
106+
function genTagFromFileName(name: string) {
107+
const fileName = name.replace(/\.yaml|twilio_|\.json/g, "");
108+
const parts = fileName.split("_");
109+
return parts.reduce((a, b) => {
110+
if (/v\d+/.test(b)) {
111+
return `${a}(${b})`;
112+
}
113+
return a + _.upperFirst(b);
114+
}, "");
115+
}
116+
117+
export function dirToSpecList(specDir: string) {
118+
const specList: MultiOpenApiSpecItem[] = [];
119+
120+
const start = performance.now();
121+
const specFiles = fs.readdirSync(specDir);
122+
specFiles.forEach((specFile) => {
123+
const spec = readYaml(path.join(specDir, specFile));
124+
const tag = genTagFromFileName(specFile);
125+
appendTags(spec, tag);
126+
specList.push({
127+
id: tag,
128+
spec,
129+
});
130+
});
131+
logger.info("spec list loaded %s, duration: %d ms",specDir, performance.now() - start);
132+
return specList;
133+
}

server/node-service/src/plugins/asana/index.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readYaml } from "../../common/util";
1+
import { readYaml, specsToOptions, version2spec } from "../../common/util";
22
import _ from "lodash";
33
import path from "path";
44
import { OpenAPIV3, OpenAPI } from "openapi-types";
@@ -8,7 +8,9 @@ import { parseOpenApi, ParseOpenApiOptions } from "../openApi/parse";
88

99

1010
const spec = readYaml(path.join(__dirname, "./asana.spec.yaml"));
11-
11+
const specs = {
12+
"v1.0": spec,
13+
}
1214

1315
const dataSourceConfig = {
1416
type: "dataSource",
@@ -23,7 +25,15 @@ const dataSourceConfig = {
2325
"key": "personalAccessToken.value",
2426
"label": "Token",
2527
"tooltip": "A [personal access token](https://developers.asana.com/docs/personal-access-token) allows access to the api for the user who created it. This should be kept a secret and be treated like a password.",
26-
}
28+
},
29+
{
30+
label: "Spec Version",
31+
key: "specVersion",
32+
type: "select",
33+
tooltip: "Version of the spec file.",
34+
placeholder: "v1.0",
35+
options: specsToOptions(specs)
36+
},
2737
]
2838
} as const;
2939

@@ -41,8 +51,8 @@ const asanaPlugin: DataSourcePlugin<any, DataSourceConfigType> = {
4151
icon: "asana.svg",
4252
category: "Project Management",
4353
dataSourceConfig,
44-
queryConfig: async () => {
45-
const { actions, categories } = await parseOpenApi(spec as OpenAPI.Document, parseOptions);
54+
queryConfig: async (data) => {
55+
const { actions, categories } = await parseOpenApi(version2spec(specs, data.specVersion) as OpenAPI.Document, parseOptions);
4656
return {
4757
type: "query",
4858
label: "Action",
@@ -58,8 +68,9 @@ const asanaPlugin: DataSourcePlugin<any, DataSourceConfigType> = {
5868
url: "",
5969
serverURL: "",
6070
dynamicParamsConfig: dataSourceConfig,
71+
specVersion: dataSourceConfig.specVersion,
6172
};
62-
return runOpenApi(actionData, runApiDsConfig, spec as OpenAPIV3.Document);
73+
return runOpenApi(actionData, runApiDsConfig, version2spec(specs, dataSourceConfig.specVersion) as OpenAPIV3.Document);
6374
},
6475
};
6576

server/node-service/src/plugins/circleCi/index.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { runOpenApi } from "../openApi";
55
import { parseOpenApi, ParseOpenApiOptions } from "../openApi/parse";
66

77
import spec from "./circleCi.spec.json";
8+
import { specsToOptions, version2spec } from "../../common/util";
9+
const specs = {
10+
"v1.0": spec,
11+
}
812

913
const dataSourceConfig = {
1014
type: "dataSource",
@@ -16,6 +20,14 @@ const dataSourceConfig = {
1620
tooltip:
1721
"[Personal API Token](https://circleci.com/docs/managing-api-tokens/#creating-a-personal-api-token)",
1822
},
23+
{
24+
label: "Spec Version",
25+
key: "specVersion",
26+
type: "select",
27+
tooltip: "Version of the spec file.",
28+
placeholder: "v1.0",
29+
options: specsToOptions(specs)
30+
},
1931
],
2032
} as const;
2133

@@ -33,9 +45,9 @@ const circleCiPlugin: DataSourcePlugin<any, DataSourceConfigType> = {
3345
icon: "circleCI.svg",
3446
category: "DevOps",
3547
dataSourceConfig,
36-
queryConfig: async () => {
48+
queryConfig: async (data) => {
3749
const { actions, categories } = await parseOpenApi(
38-
spec as unknown as OpenAPI.Document,
50+
version2spec(specs, data.specVersion) as unknown as OpenAPI.Document,
3951
parseOptions
4052
);
4153
return {
@@ -53,8 +65,9 @@ const circleCiPlugin: DataSourcePlugin<any, DataSourceConfigType> = {
5365
url: "",
5466
serverURL: "",
5567
dynamicParamsConfig: dataSourceConfig,
68+
specVersion: dataSourceConfig.specVersion,
5669
};
57-
return runOpenApi(actionData, runApiDsConfig, spec as unknown as OpenAPIV3.Document);
70+
return runOpenApi(actionData, runApiDsConfig, version2spec(specs, dataSourceConfig.specVersion) as unknown as OpenAPIV3.Document);
5871
},
5972
};
6073

server/node-service/src/plugins/cloudinary/index.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readYaml } from "../../common/util";
1+
import { readYaml, specsToOptions, version2spec } from "../../common/util";
22
import _ from "lodash";
33
import path from "path";
44
import { OpenAPI } from "openapi-types";
@@ -14,6 +14,9 @@ const specList = [
1414
{ spec: adminApiSpec, id: "admin" },
1515
{ spec: uploadApiSpec, id: "upload" },
1616
];
17+
const specs = {
18+
"v1.0": specList,
19+
}
1720

1821
const dataSourceConfig = {
1922
type: "dataSource",
@@ -32,6 +35,14 @@ const dataSourceConfig = {
3235
tooltip: "Basic auth password",
3336
placeholder: "<Basic Auth Password>",
3437
},
38+
{
39+
label: "Spec Version",
40+
key: "specVersion",
41+
type: "select",
42+
tooltip: "Version of the spec file.",
43+
placeholder: "v1.0",
44+
options: specsToOptions(specs)
45+
},
3546
],
3647
} as const;
3748

@@ -46,18 +57,18 @@ const parseOptions: ParseOpenApiOptions = {
4657

4758
type DataSourceConfigType = ConfigToType<typeof dataSourceConfig>;
4859

49-
let queryConfig: QueryConfig;
60+
let queryConfig: any = {};
5061

5162
const cloudinaryPlugin: DataSourcePlugin<any, DataSourceConfigType> = {
5263
id: "cloudinary",
5364
name: "Cloudinary",
5465
icon: "cloudinary.svg",
5566
category: "Assets",
5667
dataSourceConfig,
57-
queryConfig: async () => {
58-
if (!queryConfig) {
59-
const { actions, categories } = await parseMultiOpenApi(specList, parseOptions);
60-
queryConfig = {
68+
queryConfig: async (data) => {
69+
if (!queryConfig[data.specVersion as keyof typeof queryConfig]) {
70+
const { actions, categories } = await parseMultiOpenApi(version2spec(specs, data.specVersion), parseOptions);
71+
queryConfig[data.specVersion as keyof typeof queryConfig] = {
6172
type: "query",
6273
label: "Action",
6374
categories: {
@@ -67,15 +78,16 @@ const cloudinaryPlugin: DataSourcePlugin<any, DataSourceConfigType> = {
6778
actions,
6879
};
6980
}
70-
return queryConfig;
81+
return queryConfig[data.specVersion as keyof typeof queryConfig];
7182
},
7283
run: function (actionData, dataSourceConfig): Promise<any> {
7384
const runApiDsConfig = {
7485
url: "",
7586
serverURL: "",
7687
dynamicParamsConfig: dataSourceConfig,
88+
specVersion: dataSourceConfig.specVersion,
7789
};
78-
return runOpenApi(actionData, runApiDsConfig, specList);
90+
return runOpenApi(actionData, runApiDsConfig, version2spec(specs, dataSourceConfig.specVersion));
7991
},
8092
};
8193

server/node-service/src/plugins/couchdb/index.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { ConfigToType, DataSourcePlugin } from "lowcoder-sdk/dataSource";
44
import { runOpenApi } from "../openApi";
55
import { defaultParseOpenApiOptions, parseOpenApi, ParseOpenApiOptions } from "../openApi/parse";
66
import spec from "./CouchDB-3.1.1-resolved.json";
7-
7+
import { specsToOptions, version2spec } from "../../common/util";
8+
const specs = {
9+
"v1.0": spec,
10+
}
811
const dataSourceConfig = {
912
type: "dataSource",
1013
params: [
@@ -38,6 +41,14 @@ const dataSourceConfig = {
3841
tooltip: "",
3942
placeholder: "",
4043
},
44+
{
45+
label: "Spec Version",
46+
key: "specVersion",
47+
type: "select",
48+
tooltip: "Version of the spec file.",
49+
placeholder: "v1.0",
50+
options: specsToOptions(specs)
51+
},
4152
],
4253
} as const;
4354

@@ -59,8 +70,8 @@ const couchdbPlugin: DataSourcePlugin<any, DataSourceConfigType> = {
5970
icon: "couchdb.svg",
6071
category: "database",
6172
dataSourceConfig,
62-
queryConfig: async () => {
63-
const { actions, categories } = await parseOpenApi(spec as OpenAPI.Document, parseOptions);
73+
queryConfig: async (data) => {
74+
const { actions, categories } = await parseOpenApi(version2spec(specs, data.specVersion) as OpenAPI.Document, parseOptions);
6475
return {
6576
type: "query",
6677
label: "Operation",
@@ -77,8 +88,9 @@ const couchdbPlugin: DataSourcePlugin<any, DataSourceConfigType> = {
7788
url: "",
7889
serverURL: serverURL,
7990
dynamicParamsConfig: otherDataSourceConfig,
91+
specVersion: dataSourceConfig.specVersion
8092
};
81-
return runOpenApi(actionData, runApiDsConfig, spec as OpenAPIV2.Document);
93+
return runOpenApi(actionData, runApiDsConfig, version2spec(specs, dataSourceConfig.specVersion) as OpenAPIV2.Document);
8294
},
8395
};
8496

0 commit comments

Comments
 (0)