apass-opensdk-hugong 1.0.0 → 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/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  const Users = require('./opensdk/users')
2
2
  const Document = require('./opensdk/document')
3
+ const Utils = require('./utils/index')
4
+ const Employee = require('./opensdk/employee')
5
+ const Contract = require('./opensdk/contract')
6
+
3
7
  class HG{
4
8
  #logger
5
9
  #app_id
@@ -8,6 +12,10 @@ class HG{
8
12
  this.#logger = logger
9
13
  this.users = new Users(logger, this)
10
14
  this.document = new Document(logger, this)
15
+ this.employee = new Employee(logger, this)
16
+ this.contract = new Contract(logger, this)
17
+
18
+ this.utils = new Utils(logger, this)
11
19
  this._time = null
12
20
  }
13
21
  /**
@@ -62,41 +70,133 @@ class HG{
62
70
  this.#app_id = app_id
63
71
  this.#app_secret = app_secret
64
72
  }
73
+
65
74
  /**
66
75
  * 开平token
67
76
  * @returns
68
77
  */
69
78
  async getToken(){
70
- const result = await this.request('https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal',{ app_id: this.#app_id, app_secret: this.#app_secret})
79
+ const result = await this.request('https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal',{ app_id: this.#app_id, app_secret: this.#app_secret},false)
71
80
  return result.tenant_access_token
72
81
  }
73
82
 
83
+ async requestGet(url, params, headers=true){
84
+ return this.request([url, params], null, headers, 'GET')
85
+ }
86
+
74
87
  /**
75
88
  * 统一网络请求
76
89
  * @param {*} url
90
+ * @param {*} params
77
91
  * @param {*} data
78
92
  * @param {*} headers
93
+ * @param {*} method
79
94
  * @returns
80
95
  */
81
- async request(url, data, headers){
82
- let HGheader = { 'Content-Type': 'application/json; charset=utf-8' }
96
+ async request(url, data, headers = true, method = 'POST'){
97
+ let newHeaders = { 'Content-Type': 'application/json; charset=utf-8' }
83
98
  if(typeof headers === 'boolean' && headers === true){
84
- HGheader['Authorization'] = `Bearer ${ await this.getToken() }`
99
+ newHeaders['Authorization'] = `Bearer ${ await this.getToken() }`
85
100
  }else{
86
- HGheader = { ...HGheader,...(headers || {}) }
101
+ newHeaders = { ...newHeaders,...(headers || {}) }
102
+ }
103
+ this.#logger.log('req=',url,data,newHeaders)
104
+ let _url = url
105
+ let params = null
106
+ if(Array.isArray(url)){
107
+ _url = url[0]
108
+ params = url[1]
87
109
  }
88
- this.#logger.log('req=',url,data,HGheader)
89
110
  return new Promise((r,s)=>{
90
111
  const now = Date.now()
91
- axios({ method: 'POST', url, data, headers: HGheader}).then(response=>{
112
+ axios({ method, params, url: _url, data, headers: newHeaders}).then(response=>{
92
113
  this.#logger.info('resp=',(Date.now() - now) / 1000, 's',response.status, response.statusText,response.data)
93
114
  r(response.data)
94
115
  }).catch(e=>{
116
+ console.log(e)
95
117
  this.#logger.error('resp=',(Date.now() - now) / 1000, 's',e.response.data)
96
118
  s(e.response.data)
97
119
  })
98
120
  })
99
121
  }
122
+ /**
123
+ * 分页返回开放平台数据
124
+ * @param {*} url
125
+ * @param {*} params
126
+ * @param {*} callback callback(items) 结果回调(根据总数可能多次调用)- 可选
127
+ * @returns 如果callback为传递则一次性返回所有的数据
128
+ */
129
+ async paginatedSearchGet(url,params,callback){
130
+ return this.paginatedSearch(url, params, null, callback, 'GET')
131
+ }
132
+ /**
133
+ * 分页返回开放平台数据
134
+ * @param {*} url
135
+ * @param {*} params
136
+ * @param {*} data
137
+ * @param {*} callback callback(items) 结果回调(根据总数可能多次调用)- 可选
138
+ * @returns 如果callback为传递则一次性返回所有的数据
139
+ */
140
+ async paginatedSearch(url,params,data,callback,method='POST'){
141
+ let list = []
142
+ let page_token = null
143
+ let has_more = false
144
+ do {
145
+ const result = await this.request([url, {...(params || {page_size: 100}), page_token}],data,true,method)
146
+ page_token = result.data.page_token
147
+ has_more = result.data.has_more
148
+ if(callback){
149
+ await callback(result.data.items || [])
150
+ }else{
151
+ list.push(...(result.data.items || []))
152
+ }
153
+ } while (has_more);
154
+ return list
155
+ }
156
+
157
+ async getAllUser(field){
158
+ const list = []
159
+ await application.data.object('_user').select(field || ['_id','_name']).findStream(async records=>list.push(...records))
160
+ return list
161
+ }
162
+
163
+ /**
164
+ * 生成多语言对象
165
+ * @param {*} zh
166
+ * @param {*} en
167
+ * @returns
168
+ */
169
+ toMultilingual(zh,en){
170
+ if(Array.isArray(zh)){
171
+ //开放平台一般返回内容
172
+ if(zh[0].hasOwnProperty('lang')){
173
+ const _zh = this.toSafeValue(zh.find(it=>it.lang == 'zh-CN'),'value')
174
+ const _en = this.toSafeValue(zh.find(it=>it.lang == 'en-US'),'value')
175
+ return new application.constants.type.Multilingual({ zh: _zh || _en, en: _en || _zh })
176
+ }
177
+ }
178
+ return new application.constants.type.Multilingual({ zh: zh || en, en: en || zh })
179
+ }
180
+
181
+ /**
182
+ * 安全的取值
183
+ * @param {*} obj
184
+ * @param {*} key
185
+ * @param {*} defValue
186
+ * @returns
187
+ */
188
+ toSafeValue(obj,key,defValue){
189
+ const _obj = obj || {}
190
+ if(!_obj){
191
+ return defValue
192
+ }
193
+ if(!_obj.hasOwnProperty(key)){
194
+ return defValue
195
+ }
196
+ return _obj[key] || defValue
197
+ }
198
+
199
+
100
200
 
101
201
  }
102
202
  module.exports = HG
@@ -0,0 +1,19 @@
1
+ class Contract{
2
+ #hg = null
3
+ #logger = null
4
+ constructor(logger,hg){
5
+ this.#logger = logger
6
+ this.#hg = hg
7
+ }
8
+
9
+ /**
10
+ * 通过手机号或邮箱获取用户 ID
11
+ * @param {*} user_id_type user_id、open_id、union_id
12
+ * @returns
13
+ */
14
+ async batch_get_id(data,user_id_type){
15
+ return await this.#hg.request(`https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id?user_id_type=${user_id_type || 'open_id'}`,data,true)
16
+ }
17
+
18
+ }
19
+ module.exports = Contract
@@ -1,48 +1,48 @@
1
1
  class Document{
2
- #logger = null
3
- #hg = null
4
- constructor(logger,hg){
5
- this.#logger = logger
6
- this.#hg = hg
7
- }
8
- /**
9
- * 增加协作者权限 https://open.feishu.cn/document/server-docs/docs/permission/permission-member/create
10
- * @param {*} app_token app_token 多维表格 App 的唯一标识
11
- * @param {*} type 文件类型 bitable:多维表格
12
- * @param {*} data {
13
- "member_type": "openchat",
14
- "member_id": "oc_b962e8debdc37712a1d60bb97087a8e8",
15
- "perm": "edit",
16
- "perm_type": "container",
17
- "type": "chat"
18
- }
19
- */
20
- async permissions(app_token,type,data){
21
- if(!type){
22
- throw 'type is not empty'
2
+ #logger = null
3
+ #hg = null
4
+ constructor(logger,hg){
5
+ this.#logger = logger
6
+ this.#hg = hg
7
+ }
8
+ /**
9
+ * 增加协作者权限 https://open.feishu.cn/document/server-docs/docs/permission/permission-member/create
10
+ * @param {*} app_token app_token 多维表格 App 的唯一标识
11
+ * @param {*} type 文件类型 bitable:多维表格
12
+ * @param {*} data {
13
+ "member_type": "openchat",
14
+ "member_id": "oc_b962e8debdc37712a1d60bb97087a8e8",
15
+ "perm": "edit",
16
+ "perm_type": "container",
17
+ "type": "chat"
23
18
  }
24
- return await this.#hg.request(`https://open.feishu.cn/open-apis/drive/v1/permissions/${app_token}/members?type=${type}&need_notification=false`,data,true)
25
- }
26
- /**
27
- * 复制多维表格 https://open.feishu.cn/document/server-docs/docs/bitable-v1/app/copy
28
- * @param {*} app_token 要复制的多维表格 App 的唯一标识
29
- * @param {*} name 多维表格 App 名称。最长为 255 个字符
30
- * @param {*} folder_token 文件夹
31
- * @returns
32
- */
33
- async copy(app_token,name,folder_token,without_content,time_zone){
34
- return await this.#hg.request(`https://open.feishu.cn/open-apis/bitable/v1/apps/${app_token}/copy`,{ name,folder_token,without_content: without_content || false,time_zone: time_zone || 'Asia/Shanghai' },true)
19
+ */
20
+ async permissions(app_token,type,data){
21
+ if(!type){
22
+ throw 'type is not empty'
35
23
  }
36
- /**
37
- * 创建多维表格 https://open.feishu.cn/document/server-docs/docs/bitable-v1/app/create
38
- * @param {*} name 多维表格 App 名称
39
- * @param {*} folder_token 多维表格 App 归属文件夹
40
- * @param {*} time_zone
41
- * @returns
42
- */
43
- async create(name, folder_token, time_zone){
44
- return await this.#hg.request('https://open.feishu.cn/open-apis/bitable/v1/apps',{ name, folder_token, time_zone: time_zone || 'Asia/Shanghai' },true)
45
- }
46
-
24
+ return await this.#hg.request(`https://open.feishu.cn/open-apis/drive/v1/permissions/${app_token}/members?type=${type}&need_notification=false`,data,true)
25
+ }
26
+ /**
27
+ * 复制多维表格 https://open.feishu.cn/document/server-docs/docs/bitable-v1/app/copy
28
+ * @param {*} app_token 要复制的多维表格 App 的唯一标识
29
+ * @param {*} name 多维表格 App 名称。最长为 255 个字符
30
+ * @param {*} folder_token 文件夹
31
+ * @returns
32
+ */
33
+ async copy(app_token,name,folder_token,without_content,time_zone){
34
+ return await this.#hg.request(`https://open.feishu.cn/open-apis/bitable/v1/apps/${app_token}/copy`,{ name,folder_token,without_content: without_content || false,time_zone: time_zone || 'Asia/Shanghai' },true)
35
+ }
36
+ /**
37
+ * 创建多维表格 https://open.feishu.cn/document/server-docs/docs/bitable-v1/app/create
38
+ * @param {*} name 多维表格 App 名称
39
+ * @param {*} folder_token 多维表格 App 归属文件夹
40
+ * @param {*} time_zone
41
+ * @returns
42
+ */
43
+ async create(name, folder_token, time_zone){
44
+ return await this.#hg.request('https://open.feishu.cn/open-apis/bitable/v1/apps',{ name, folder_token, time_zone: time_zone || 'Asia/Shanghai' },true)
47
45
  }
48
- module.exports = Document
46
+
47
+ }
48
+ module.exports = Document
@@ -0,0 +1,20 @@
1
+ class Employee{
2
+ #hg = null
3
+ #logger = null
4
+ constructor(logger,hg){
5
+ this.#logger = logger
6
+ this.#hg = hg
7
+ }
8
+
9
+ /**
10
+ * 搜索员工信息
11
+ * @param {*} params
12
+ * @param {*} data
13
+ * @param {*} callback(items) 结果回调(根据总数可能多次调用)
14
+ */
15
+ async search(params,data,callback){
16
+ return await this.#hg.paginatedSearch(`https://open.feishu.cn/open-apis/corehr/v2/employees/search`,params,data,callback)
17
+ }
18
+
19
+ }
20
+ module.exports = Employee
package/opensdk/users.js CHANGED
@@ -1,19 +1,19 @@
1
1
  class Users{
2
- #hg = null
3
- #logger = null
4
- constructor(logger,hg){
5
- this.#logger = logger
6
- this.#hg = hg
7
- }
8
-
9
- /**
10
- * 通过手机号或邮箱获取用户 ID
11
- * @param {*} user_id_type user_id、open_id、union_id
12
- * @returns
13
- */
14
- async batch_get_id(data,user_id_type){
15
- return await this.#hg.request(`https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id?user_id_type=${user_id_type || 'open_id'}`,data,true)
16
- }
2
+ #hg = null
3
+ #logger = null
4
+ constructor(logger,hg){
5
+ this.#logger = logger
6
+ this.#hg = hg
7
+ }
17
8
 
9
+ /**
10
+ * 通过手机号或邮箱获取用户 ID
11
+ * @param {*} user_id_type user_id、open_id、union_id
12
+ * @returns
13
+ */
14
+ async batch_get_id(data,user_id_type){
15
+ return await this.#hg.request(`https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id?user_id_type=${user_id_type || 'open_id'}`,data,true)
18
16
  }
19
- module.exports = Users
17
+
18
+ }
19
+ module.exports = Users
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apass-opensdk-hugong",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "飞书Apass低代码平台-飞书开放平台-相关的接口整合和常用的方法整合",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/readme.md ADDED
@@ -0,0 +1,133 @@
1
+ # apass-opensdk-hugong
2
+ ## 简介
3
+ apass-opensdk-hugong 是一个基于apass-opensdk的封装,提供了一些常用的功能
4
+ - 什么人可以使用这个SDK
5
+ - 从事飞书低代码平台(APASS)开发人员.
6
+
7
+ ## 安装
8
+
9
+ ```
10
+ 在飞书低代码平台(云函数)依赖管理-右侧-搜索: apass-opensdk-hugong
11
+ ```
12
+
13
+ ## 如何使用
14
+ ```
15
+ const Hugong = require('apass-opensdk-hugong');
16
+ 初始化
17
+ const hg = new Hugong(logger)
18
+ ```
19
+ ## 常用
20
+ ```
21
+ 显示运行时间
22
+ hg.newTime()
23
+ hg.printTime()
24
+
25
+ 线程睡眠(毫秒)
26
+ await hg.sleep(2000)
27
+
28
+ 支持将数组分割成指定长度分段数组
29
+ 第一种用法,一次性返还分割后的数组
30
+ const list = hg.utils.splitArray([], 50) 输出 [ [50],[50] ]
31
+
32
+ 第二种用法,每次输出50条 处理完成后继续下次执行
33
+ await hg.utils.splitArray([], 10, async (items)=>{
34
+ // do something
35
+ })
36
+
37
+ ```
38
+ ## 文件上传/下载
39
+
40
+ ```
41
+ 1)从网络下载文件后上传到飞书租户空间,返回上传后的文件信息
42
+ await hg.utils.file.downloadFileToUpload(url)
43
+ 2)如果有鉴权, 可以传递header参数
44
+ await hg.utils.file.downloadFileToUpload(url,{ Authorization: `...`})
45
+ 3)保存数据到本地环境中,比如接口返回的数据需要保存到本地环境中方便查看
46
+ saveDataToEnv(data,path)
47
+
48
+ 1)从飞书租户空间下载文件到本地环境中
49
+ file_info={ id, mime_type, name, ...}
50
+ file_path=存储地址可选,不填写则默认当前时间戳
51
+ await hg.utils.file.downloadFileToTmp(fileInfo,file_path)
52
+
53
+ 2)解析csv文件,file_path必须是本地环境的文件路径 /tep/aaa.csv 可以使用上面的方法downloadFileToTmp下载文件到本地环境中
54
+ await hg.utils.file.csvRead(file_path,callback)
55
+ 示例1 读取完成后返回数组
56
+ const list = await hg.utils.file.csvRead(file_path)
57
+ 示例2 读取完成后回调
58
+ await hg.utils.file.csvRead(file_path,async (row)=>{
59
+ // do something
60
+ })
61
+ ```
62
+
63
+ ## 小工具/多语言
64
+
65
+ ```
66
+ 对象数据新增多语言对象
67
+
68
+ 生成多语言对象
69
+ hg.toMultilingual([{ lang: 'en-US', value: 'Regular' },{ lang: 'zh-CN', value: '正式' }]) 常用于开放平台返回的多语言数据转换
70
+ hg.toMultilingual(zh,en)
71
+
72
+ 安全的取值
73
+ hg.toSafeValue(obj={},key,defValue)
74
+
75
+ ```
76
+
77
+ ## 飞书人事
78
+
79
+ 搜索员工信息
80
+
81
+ ```
82
+ 设置appid和appsecret
83
+ await hg.setAppId('cli_000000000','0000000000000')
84
+
85
+ /**
86
+ * 搜索员工信息
87
+ * @param {*} params
88
+ * @param {*} data
89
+ * @param {*} callback callback(items) 结果回调(根据总数可能多次调用)- 可选
90
+ * @returns 如果callback为传递则一次性返回所有的数据
91
+ */
92
+ await hg.employee.search(params,data,callback)
93
+
94
+ // 示例[具体参数请参考开放平台定义](https://open.feishu.cn/document/server-docs/corehr-v1/employee/search)
95
+ await hg.employee.search({ page_size:100,user_id_type:'user_id',},{ /* 要查询的字段 */}, async (items)=>{
96
+ // do something 假设服务端有300条数据,每次返回100条,会调用3次, 如果希望一次性返回所有数据,callback传null即可
97
+ })
98
+ const list = await hg.employee.search(params,data) //这将一次性返回所有数据
99
+ ```
100
+ ## 开放平台分页获取所有数据
101
+
102
+ 所有的分页接口都可以使用
103
+
104
+ ```
105
+ 设置appid和appsecret
106
+ await hg.setAppId('cli_000000000','0000000000000')
107
+
108
+ /**
109
+ * GET类型接口分页返回开放平台数据
110
+ * @param {*} url
111
+ * @param {*} params
112
+ * @param {*} callback callback(items) 结果回调(根据总数可能多次调用)- 可选
113
+ * @returns callback为null,则一次性返回所有的数据
114
+ */
115
+ hg.paginatedSearchGet(url,params,callback)
116
+
117
+ /**
118
+ * POST接口分页返回开放平台数据
119
+ * @param {*} url
120
+ * @param {*} params
121
+ * @param {*} data
122
+ * @param {*} callback callback(items) 结果回调(根据总数可能多次调用)- 可选
123
+ * @returns callback为null,则一次性返回所有的数据
124
+ */
125
+ await hg.paginatedSearch(url,params,data,callback)
126
+
127
+ 示例1
128
+ const list = await hg.paginatedSearchGet(url, params, data)
129
+ 示例2
130
+ await hg.paginatedSearchGet(url, params, async (items)=>{
131
+ // do something 假设服务端有300条数据,每次返回100条,会调用3次, 如果希望一次性返回所有数据,callback传null即可
132
+ })
133
+ ```
package/utils/file_.js ADDED
@@ -0,0 +1,81 @@
1
+ const fs = require('fs')
2
+ class File_{
3
+ #hg = null
4
+ #logger = null
5
+ constructor(logger,hg){
6
+ this.#logger = logger
7
+ this.#hg = hg
8
+ }
9
+ getCurrentTimeFolderName() {
10
+ const now = new Date();
11
+ const year = now.getFullYear();
12
+ const month = String(now.getMonth() + 1).padStart(2, '0');
13
+ const day = String(now.getDate()).padStart(2, '0');
14
+ const hours = String(now.getHours()).padStart(2, '0');
15
+ const minutes = String(now.getMinutes()).padStart(2, '0');
16
+ const seconds = String(now.getSeconds()).padStart(2, '0');
17
+ return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
18
+ }
19
+ /**
20
+ * * 网络下载文件并上传到租户空间
21
+ * @param {*} url 网络文件地址
22
+ * @param {*} header 可选
23
+ * @returns
24
+ */
25
+ async downloadFileToUpload(url, header = {}){
26
+ this.#logger.log(`download url=${url}`)
27
+ const resp = await axios({ url, method: 'get', responseType: 'stream', headers: {...header} });
28
+ // 上传文件获取文件 token
29
+ const file_info = await application.resources.file.upload(resp.data);
30
+ this.#logger.log('file_info',file_info)
31
+ return file_info
32
+ }
33
+ /**
34
+ * 下载租户空间的文件到环境tmp目录中
35
+ * @param {*} file_info { id, mime_type, name, ...}
36
+ * @param {*} file_path 存储地址可选,不填写则默认当前时间戳
37
+ * @returns
38
+ */
39
+ async downloadFileToTmp(file_info, file_path){
40
+ this.#logger.log(`download input=${ JSON.stringify(file_info)}`)
41
+ const _file_path = file_path || `/tmp/${Date.now()}.${ file_info.mime_type }`
42
+ await application.resources.file.download({id: file_info.id}, _file_path);
43
+ this.#logger.log(`download success, path=` + _file_path)
44
+ return _file_path
45
+ }
46
+ /**
47
+ * 写入文件
48
+ * @param {*} data 写入的内容
49
+ * @param {*} fileName 存储地址
50
+ */
51
+ saveDataToEnv(data,fileName){
52
+ fs.writeFileSync(fileName || `${ Date.now() }.txt`,data, { flag: 'a' })
53
+ }
54
+ /**
55
+ * 读取Excel文件(需要安装依赖:csv-parser)
56
+ * @param {*} path
57
+ * @param {*} callback(row) 每一行的回调
58
+ * @returns callback为null时,返回全部读取的结果list
59
+ */
60
+ async csvRead(path,callback) {
61
+ const csv = require('csv-parser');
62
+ return await new Promise((resolve, reject) => {
63
+ const list = []
64
+ fs.createReadStream(path).pipe(csv())
65
+ .on('data', (row) => {
66
+ if(callback){
67
+ callback(row)
68
+ }else{
69
+ list.push(row)
70
+ }
71
+ })
72
+ .on('end', () => {
73
+ resolve(list)
74
+ })
75
+ .on('error',()=>{
76
+ reject()
77
+ });
78
+ })
79
+ }
80
+ }
81
+ module.exports = File_
package/utils/index.js ADDED
@@ -0,0 +1,41 @@
1
+ const Url = require('./url')
2
+ const File_ = require('./file_')
3
+ class Utils{
4
+ #hg = null
5
+ #logger = null
6
+ constructor(logger,hg){
7
+ this.#logger = logger
8
+ this.#hg = hg
9
+
10
+ this.url = new Url(logger,hg)
11
+ this.file = new File_(logger,hg)
12
+ }
13
+
14
+ /**
15
+ * 数组分割
16
+ * @param {*} list
17
+ * @param {*} chunkSize 分割长度
18
+ * @param {*} callback(item) 分割后回调(不传递则方法返回切割长度后的数组)
19
+ * @returns
20
+ */
21
+ async splitArray(list, chunkSize, callback) {
22
+ const result = [];
23
+ for (let i = 0; i < list.length; i += chunkSize) {
24
+ const batch = list.slice(i, i + chunkSize)
25
+ if(callback){
26
+ await callback(batch)
27
+ continue
28
+ }
29
+ result.push(batch); // 使用 slice 方法按 chunkSize 进行切割
30
+ }
31
+ return result;
32
+ }
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+ }
41
+ module.exports = Utils
package/utils/url.js ADDED
@@ -0,0 +1,20 @@
1
+
2
+ class Url{
3
+ #hg = null
4
+ #logger = null
5
+ constructor(logger,hg){
6
+ this.#logger = logger
7
+ this.#hg = hg
8
+ }
9
+
10
+ jsonToUrlParams(jsonData) {
11
+ const params = new URLSearchParams();
12
+ for (const key in jsonData) {
13
+ if (jsonData[key]) {
14
+ params.append(key, jsonData[key]);
15
+ }
16
+ }
17
+ return params.toString();
18
+ }
19
+ }
20
+ module.exports = Url