@platformatic/sql-events 0.5.0
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/.nyc_output/216765ce-d0ee-4972-a798-60b27dee3411.json +1 -0
- package/.nyc_output/b818e2e5-299b-49c9-ade4-114a2b3e4fe5.json +1 -0
- package/.nyc_output/bd3970ed-e41b-4f49-8852-b8436108783a.json +1 -0
- package/.nyc_output/c2aec7f4-8b75-4b4a-8d27-d834cf4b0a33.json +1 -0
- package/.nyc_output/processinfo/216765ce-d0ee-4972-a798-60b27dee3411.json +1 -0
- package/.nyc_output/processinfo/b818e2e5-299b-49c9-ade4-114a2b3e4fe5.json +1 -0
- package/.nyc_output/processinfo/bd3970ed-e41b-4f49-8852-b8436108783a.json +1 -0
- package/.nyc_output/processinfo/c2aec7f4-8b75-4b4a-8d27-d834cf4b0a33.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/.taprc +1 -0
- package/LICENSE +201 -0
- package/index.d.ts +30 -0
- package/index.js +131 -0
- package/package.json +42 -0
- package/test/helper.js +66 -0
- package/test/hooks.test.js +317 -0
- package/test/redis.test.js +116 -0
- package/test/simple.test.js +305 -0
- package/test/types/index.test-d.ts +17 -0
package/test/helper.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Needed to work with dates & postgresql
|
|
4
|
+
// See https://node-postgres.com/features/types/
|
|
5
|
+
process.env.TZ = 'UTC'
|
|
6
|
+
|
|
7
|
+
const connInfo = {}
|
|
8
|
+
|
|
9
|
+
if (!process.env.DB || process.env.DB === 'postgresql') {
|
|
10
|
+
connInfo.connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
|
|
11
|
+
module.exports.isPg = true
|
|
12
|
+
} else if (process.env.DB === 'mariadb') {
|
|
13
|
+
connInfo.connectionString = 'mysql://root@127.0.0.1:3307/graph'
|
|
14
|
+
connInfo.poolSize = 10
|
|
15
|
+
module.exports.isMysql = true
|
|
16
|
+
} else if (process.env.DB === 'mysql') {
|
|
17
|
+
connInfo.connectionString = 'mysql://root@127.0.0.1/graph'
|
|
18
|
+
connInfo.poolSize = 10
|
|
19
|
+
module.exports.isMysql = true
|
|
20
|
+
} else if (process.env.DB === 'mysql8') {
|
|
21
|
+
connInfo.connectionString = 'mysql://root@127.0.0.1:3308/graph'
|
|
22
|
+
connInfo.poolSize = 10
|
|
23
|
+
module.exports.isMysql = true
|
|
24
|
+
} else if (process.env.DB === 'sqlite') {
|
|
25
|
+
connInfo.connectionString = 'sqlite://:memory:'
|
|
26
|
+
module.exports.isSQLite = true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports.connInfo = connInfo
|
|
30
|
+
|
|
31
|
+
module.exports.clear = async function (db, sql) {
|
|
32
|
+
try {
|
|
33
|
+
await db.query(sql`DROP TABLE pages`)
|
|
34
|
+
} catch (err) {
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await db.query(sql`DROP TABLE categories`)
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await db.query(sql`DROP TABLE posts`)
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await db.query(sql`DROP TABLE simple_types`)
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await db.query(sql`DROP TABLE owners`)
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await db.query(sql`DROP TABLE users`)
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await db.query(sql`DROP TABLE versions`)
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('tap')
|
|
4
|
+
const sqlMapper = require('@platformatic/sql-mapper')
|
|
5
|
+
const { connect } = sqlMapper
|
|
6
|
+
const { clear, connInfo, isSQLite } = require('./helper')
|
|
7
|
+
const sqlEvents = require('..')
|
|
8
|
+
const { setupEmitter } = sqlEvents
|
|
9
|
+
const MQEmitter = require('mqemitter')
|
|
10
|
+
|
|
11
|
+
const fakeLogger = {
|
|
12
|
+
trace () {},
|
|
13
|
+
error () {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('get topics', async ({ equal, same, teardown }) => {
|
|
17
|
+
async function onDatabaseLoad (db, sql) {
|
|
18
|
+
await clear(db, sql)
|
|
19
|
+
teardown(() => db.dispose())
|
|
20
|
+
|
|
21
|
+
if (isSQLite) {
|
|
22
|
+
await db.query(sql`CREATE TABLE pages (
|
|
23
|
+
id INTEGER PRIMARY KEY,
|
|
24
|
+
title VARCHAR(42)
|
|
25
|
+
);`)
|
|
26
|
+
} else {
|
|
27
|
+
await db.query(sql`CREATE TABLE pages (
|
|
28
|
+
id SERIAL PRIMARY KEY,
|
|
29
|
+
title VARCHAR(255) NOT NULL
|
|
30
|
+
);`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const mapper = await connect({
|
|
34
|
+
log: fakeLogger,
|
|
35
|
+
...connInfo,
|
|
36
|
+
onDatabaseLoad
|
|
37
|
+
})
|
|
38
|
+
const pageEntity = mapper.entities.page
|
|
39
|
+
|
|
40
|
+
const mq = MQEmitter()
|
|
41
|
+
equal(setupEmitter({ mapper, mq, log: fakeLogger }), undefined)
|
|
42
|
+
const queue = await mapper.subscribe([
|
|
43
|
+
await pageEntity.getSubscriptionTopic({ action: 'save' }),
|
|
44
|
+
await pageEntity.getSubscriptionTopic({ action: 'delete' })
|
|
45
|
+
])
|
|
46
|
+
equal(mapper.mq, mq)
|
|
47
|
+
|
|
48
|
+
const expected = []
|
|
49
|
+
|
|
50
|
+
// save - new record
|
|
51
|
+
const page = await pageEntity.save({
|
|
52
|
+
input: { title: 'fourth page' }
|
|
53
|
+
})
|
|
54
|
+
expected.push({
|
|
55
|
+
topic: '/entity/page/save/' + page.id,
|
|
56
|
+
payload: {
|
|
57
|
+
id: page.id
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// save - update record
|
|
62
|
+
await pageEntity.save({
|
|
63
|
+
input: {
|
|
64
|
+
id: page.id,
|
|
65
|
+
title: 'fifth page'
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
expected.push({
|
|
69
|
+
topic: '/entity/page/save/' + page.id,
|
|
70
|
+
payload: {
|
|
71
|
+
id: page.id
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
await pageEntity.delete({
|
|
76
|
+
where: {
|
|
77
|
+
id: {
|
|
78
|
+
eq: page.id
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
fields: ['id', 'title']
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
expected.push({
|
|
85
|
+
topic: '/entity/page/delete/' + page.id,
|
|
86
|
+
payload: {
|
|
87
|
+
id: page.id
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
for await (const ev of queue) {
|
|
92
|
+
same(ev, expected.shift())
|
|
93
|
+
if (expected.length === 0) {
|
|
94
|
+
break
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('hooks', async ({ equal, same, teardown }) => {
|
|
100
|
+
async function onDatabaseLoad (db, sql) {
|
|
101
|
+
await clear(db, sql)
|
|
102
|
+
teardown(() => db.dispose())
|
|
103
|
+
|
|
104
|
+
if (isSQLite) {
|
|
105
|
+
await db.query(sql`CREATE TABLE pages (
|
|
106
|
+
id INTEGER PRIMARY KEY,
|
|
107
|
+
title VARCHAR(42)
|
|
108
|
+
);`)
|
|
109
|
+
} else {
|
|
110
|
+
await db.query(sql`CREATE TABLE pages (
|
|
111
|
+
id SERIAL PRIMARY KEY,
|
|
112
|
+
title VARCHAR(255) NOT NULL
|
|
113
|
+
);`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const mapper = await connect({
|
|
117
|
+
log: fakeLogger,
|
|
118
|
+
...connInfo,
|
|
119
|
+
onDatabaseLoad
|
|
120
|
+
})
|
|
121
|
+
const pageEntity = mapper.entities.page
|
|
122
|
+
|
|
123
|
+
const mq = MQEmitter()
|
|
124
|
+
equal(setupEmitter({ mapper, mq, log: fakeLogger }), undefined)
|
|
125
|
+
const queue = await mapper.subscribe([
|
|
126
|
+
await pageEntity.getSubscriptionTopic({ action: 'save' }),
|
|
127
|
+
await pageEntity.getSubscriptionTopic({ action: 'delete' })
|
|
128
|
+
])
|
|
129
|
+
equal(mapper.mq, mq)
|
|
130
|
+
|
|
131
|
+
const expected = []
|
|
132
|
+
|
|
133
|
+
// save - new record
|
|
134
|
+
const page = await pageEntity.save({
|
|
135
|
+
input: { title: 'fourth page' }
|
|
136
|
+
})
|
|
137
|
+
expected.push({
|
|
138
|
+
topic: '/entity/page/save/' + page.id,
|
|
139
|
+
payload: {
|
|
140
|
+
id: page.id
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// save - update record
|
|
145
|
+
await pageEntity.save({
|
|
146
|
+
input: {
|
|
147
|
+
id: page.id,
|
|
148
|
+
title: 'fifth page'
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
expected.push({
|
|
152
|
+
topic: '/entity/page/save/' + page.id,
|
|
153
|
+
payload: {
|
|
154
|
+
id: page.id
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
await pageEntity.delete({
|
|
159
|
+
where: {
|
|
160
|
+
id: {
|
|
161
|
+
eq: page.id
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
fields: ['id', 'title']
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
expected.push({
|
|
168
|
+
topic: '/entity/page/delete/' + page.id,
|
|
169
|
+
payload: {
|
|
170
|
+
id: page.id
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
for await (const ev of queue) {
|
|
175
|
+
same(ev, expected.shift())
|
|
176
|
+
if (expected.length === 0) {
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('get topics', async ({ equal, same, teardown }) => {
|
|
183
|
+
async function onDatabaseLoad (db, sql) {
|
|
184
|
+
await clear(db, sql)
|
|
185
|
+
teardown(() => db.dispose())
|
|
186
|
+
|
|
187
|
+
if (isSQLite) {
|
|
188
|
+
await db.query(sql`CREATE TABLE pages (
|
|
189
|
+
id INTEGER PRIMARY KEY,
|
|
190
|
+
title VARCHAR(42)
|
|
191
|
+
);`)
|
|
192
|
+
} else {
|
|
193
|
+
await db.query(sql`CREATE TABLE pages (
|
|
194
|
+
id SERIAL PRIMARY KEY,
|
|
195
|
+
title VARCHAR(255) NOT NULL
|
|
196
|
+
);`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const mapper = await connect({
|
|
200
|
+
log: fakeLogger,
|
|
201
|
+
...connInfo,
|
|
202
|
+
onDatabaseLoad
|
|
203
|
+
})
|
|
204
|
+
mapper.addEntityHooks('page', {
|
|
205
|
+
async getSubscriptionTopic (original, { action }) {
|
|
206
|
+
equal('create', action)
|
|
207
|
+
return original({ action })
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const pageEntity = mapper.entities.page
|
|
212
|
+
|
|
213
|
+
const mq = MQEmitter()
|
|
214
|
+
equal(setupEmitter({ mapper, mq, log: fakeLogger }), undefined)
|
|
215
|
+
await pageEntity.getSubscriptionTopic({ action: 'save' })
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('no events', async ({ equal, same, teardown, fail, comment }) => {
|
|
219
|
+
async function onDatabaseLoad (db, sql) {
|
|
220
|
+
await clear(db, sql)
|
|
221
|
+
teardown(() => db.dispose())
|
|
222
|
+
|
|
223
|
+
if (isSQLite) {
|
|
224
|
+
await db.query(sql`CREATE TABLE pages (
|
|
225
|
+
id INTEGER PRIMARY KEY,
|
|
226
|
+
title VARCHAR(42)
|
|
227
|
+
);`)
|
|
228
|
+
} else {
|
|
229
|
+
await db.query(sql`CREATE TABLE pages (
|
|
230
|
+
id SERIAL PRIMARY KEY,
|
|
231
|
+
title VARCHAR(255) NOT NULL
|
|
232
|
+
);`)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const mapper = await connect({
|
|
236
|
+
log: fakeLogger,
|
|
237
|
+
...connInfo,
|
|
238
|
+
onDatabaseLoad
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const mq = MQEmitter()
|
|
242
|
+
equal(setupEmitter({ mapper, mq, log: fakeLogger }), undefined)
|
|
243
|
+
|
|
244
|
+
const pageEntity = mapper.entities.page
|
|
245
|
+
|
|
246
|
+
// disable publishing
|
|
247
|
+
pageEntity.getPublishTopic = function () {
|
|
248
|
+
return false
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const queue = await mapper.subscribe([
|
|
252
|
+
await pageEntity.getSubscriptionTopic({ action: 'save' }),
|
|
253
|
+
await pageEntity.getSubscriptionTopic({ action: 'delete' })
|
|
254
|
+
])
|
|
255
|
+
equal(mapper.mq, mq)
|
|
256
|
+
|
|
257
|
+
queue.on('data', function (msg) {
|
|
258
|
+
comment(JSON.stringify(msg, null, 2))
|
|
259
|
+
fail('no message')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// save - new record
|
|
263
|
+
const page = await pageEntity.save({
|
|
264
|
+
input: { title: 'fourth page' }
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// save - update record
|
|
268
|
+
await pageEntity.save({
|
|
269
|
+
input: {
|
|
270
|
+
id: page.id,
|
|
271
|
+
title: 'fifth page'
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
// delete a record
|
|
276
|
+
await pageEntity.delete({
|
|
277
|
+
where: {
|
|
278
|
+
id: {
|
|
279
|
+
eq: page.id
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
fields: ['id', 'title']
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test('wrong action', async ({ equal, rejects, teardown, fail, comment }) => {
|
|
287
|
+
async function onDatabaseLoad (db, sql) {
|
|
288
|
+
await clear(db, sql)
|
|
289
|
+
teardown(() => db.dispose())
|
|
290
|
+
|
|
291
|
+
if (isSQLite) {
|
|
292
|
+
await db.query(sql`CREATE TABLE pages (
|
|
293
|
+
id INTEGER PRIMARY KEY,
|
|
294
|
+
title VARCHAR(42)
|
|
295
|
+
);`)
|
|
296
|
+
} else {
|
|
297
|
+
await db.query(sql`CREATE TABLE pages (
|
|
298
|
+
id SERIAL PRIMARY KEY,
|
|
299
|
+
title VARCHAR(255) NOT NULL
|
|
300
|
+
);`)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const mapper = await connect({
|
|
304
|
+
log: fakeLogger,
|
|
305
|
+
...connInfo,
|
|
306
|
+
onDatabaseLoad
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
setupEmitter({ mapper, log: fakeLogger })
|
|
310
|
+
|
|
311
|
+
const pageEntity = mapper.entities.page
|
|
312
|
+
|
|
313
|
+
equal(await pageEntity.getPublishTopic({ action: 'foo', data: { id: 42 } }), false)
|
|
314
|
+
rejects(pageEntity.getPublishTopic({ action: 'foo', data: { } }))
|
|
315
|
+
rejects(pageEntity.getPublishTopic({ action: 'foo' }))
|
|
316
|
+
rejects(pageEntity.getSubscriptionTopic({ action: 'foo' }), 'no such action foo')
|
|
317
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('tap')
|
|
4
|
+
const sqlMapper = require('@platformatic/sql-mapper')
|
|
5
|
+
const { connect } = sqlMapper
|
|
6
|
+
const { clear, connInfo, isSQLite } = require('./helper')
|
|
7
|
+
const sqlEvents = require('..')
|
|
8
|
+
const { setupEmitter } = sqlEvents
|
|
9
|
+
const MQEmitterRedis = require('mqemitter-redis')
|
|
10
|
+
const { promisify } = require('util')
|
|
11
|
+
const { PassThrough } = require('stream')
|
|
12
|
+
|
|
13
|
+
const fakeLogger = {
|
|
14
|
+
trace () {},
|
|
15
|
+
error () {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test('emit events', async ({ equal, same, teardown, comment }) => {
|
|
19
|
+
async function onDatabaseLoad (db, sql) {
|
|
20
|
+
await clear(db, sql)
|
|
21
|
+
teardown(() => db.dispose())
|
|
22
|
+
|
|
23
|
+
if (isSQLite) {
|
|
24
|
+
await db.query(sql`CREATE TABLE pages (
|
|
25
|
+
id INTEGER PRIMARY KEY,
|
|
26
|
+
title VARCHAR(42)
|
|
27
|
+
);`)
|
|
28
|
+
} else {
|
|
29
|
+
await db.query(sql`CREATE TABLE pages (
|
|
30
|
+
id SERIAL PRIMARY KEY,
|
|
31
|
+
title VARCHAR(255) NOT NULL
|
|
32
|
+
);`)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const mapper = await connect({
|
|
36
|
+
log: fakeLogger,
|
|
37
|
+
...connInfo,
|
|
38
|
+
onDatabaseLoad
|
|
39
|
+
})
|
|
40
|
+
const pageEntity = mapper.entities.page
|
|
41
|
+
|
|
42
|
+
equal(setupEmitter({ mapper, connectionString: 'redis://127.0.0.1:6379', log: fakeLogger }), undefined)
|
|
43
|
+
teardown(promisify(mapper.mq.close.bind(mapper.mq)))
|
|
44
|
+
|
|
45
|
+
const anotherMQ = new MQEmitterRedis()
|
|
46
|
+
teardown(() => anotherMQ.close())
|
|
47
|
+
|
|
48
|
+
const messages = new PassThrough({ objectMode: true })
|
|
49
|
+
await promisify(anotherMQ.on.bind(anotherMQ))('#', function (msg, cb) {
|
|
50
|
+
messages.write(msg, cb)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const queue = await mapper.subscribe([
|
|
54
|
+
'/entity/page/save/+',
|
|
55
|
+
'/entity/page/delete/+'
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
const expected = []
|
|
59
|
+
|
|
60
|
+
// save - new record
|
|
61
|
+
const page = await pageEntity.save({
|
|
62
|
+
input: { title: 'fourth page' }
|
|
63
|
+
})
|
|
64
|
+
expected.push({
|
|
65
|
+
topic: '/entity/page/save/' + page.id,
|
|
66
|
+
payload: {
|
|
67
|
+
id: page.id
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// save - update record
|
|
72
|
+
await pageEntity.save({
|
|
73
|
+
input: {
|
|
74
|
+
id: page.id,
|
|
75
|
+
title: 'fifth page'
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
expected.push({
|
|
79
|
+
topic: '/entity/page/save/' + page.id,
|
|
80
|
+
payload: {
|
|
81
|
+
id: page.id
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await pageEntity.delete({
|
|
86
|
+
where: {
|
|
87
|
+
id: {
|
|
88
|
+
eq: page.id
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
fields: ['id', 'title']
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expected.push({
|
|
95
|
+
topic: '/entity/page/delete/' + page.id,
|
|
96
|
+
payload: {
|
|
97
|
+
id: page.id
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
let i = 0
|
|
102
|
+
for await (const ev of queue) {
|
|
103
|
+
same(ev, expected[i++])
|
|
104
|
+
if (i === expected.length) {
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
i = 0
|
|
110
|
+
for await (const ev of messages) {
|
|
111
|
+
same(ev, expected[i++])
|
|
112
|
+
if (i === expected.length) {
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})
|