@live-change/url-service 0.2.45
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/definition.js +12 -0
- package/index.js +372 -0
- package/model.js +78 -0
- package/package.json +32 -0
package/definition.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const app = require("@live-change/framework").app()
|
|
2
|
+
|
|
3
|
+
const relationsPlugin = require('@live-change/relations-plugin')
|
|
4
|
+
const userService = require('@live-change/user-service')
|
|
5
|
+
const accessControlService = require('@live-change/access-control-service')
|
|
6
|
+
|
|
7
|
+
const definition = app.createServiceDefinition({
|
|
8
|
+
name: "url",
|
|
9
|
+
use: [ relationsPlugin, userService, accessControlService ]
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
module.exports = definition
|
package/index.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
const App = require("@live-change/framework")
|
|
2
|
+
const app = App.app()
|
|
3
|
+
|
|
4
|
+
const definition = require('./definition.js')
|
|
5
|
+
const config = definition.config
|
|
6
|
+
const {
|
|
7
|
+
createFromTitle = (title) => {
|
|
8
|
+
let path = title
|
|
9
|
+
path = path.replace(/[@]+/g, '-at-')
|
|
10
|
+
path = path.replace(/[_/\\\\ -]+/g, '-')
|
|
11
|
+
path = path.replace(/[^a-z0-9-]+/gi, '')
|
|
12
|
+
return path
|
|
13
|
+
},
|
|
14
|
+
urlWriterRoles = ['writer']
|
|
15
|
+
} = config
|
|
16
|
+
|
|
17
|
+
const { Canonical, Redirect, UrlToTarget } = require("./model.js")
|
|
18
|
+
|
|
19
|
+
definition.view({
|
|
20
|
+
name: "urlsByTargetAndPath",
|
|
21
|
+
properties: {
|
|
22
|
+
targetType: {
|
|
23
|
+
type: String,
|
|
24
|
+
validation: ['nonEmpty']
|
|
25
|
+
},
|
|
26
|
+
domain: {
|
|
27
|
+
type: String
|
|
28
|
+
},
|
|
29
|
+
path: {
|
|
30
|
+
type: String,
|
|
31
|
+
validation: ['nonEmpty']
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
returns: {
|
|
35
|
+
type: Object,
|
|
36
|
+
properties: {
|
|
37
|
+
urlType: {
|
|
38
|
+
type: String,
|
|
39
|
+
},
|
|
40
|
+
target: {
|
|
41
|
+
type: String
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
daoPath(params, { client, service }, method) {
|
|
46
|
+
const { targetType, domain, path } = params
|
|
47
|
+
return UrlToTarget.rangePath({ targetType, domain, path }, App.extractRange(params))
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const randomLettersBig = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
52
|
+
const randomLettersSmall = 'abcdefghijklmnopqrstuvwxyz'
|
|
53
|
+
const randomDigits = '0123456789'
|
|
54
|
+
const charsets = {
|
|
55
|
+
'all' : randomLettersBig + randomLettersSmall + randomDigits,
|
|
56
|
+
'digits': randomDigits,
|
|
57
|
+
'letters': randomLettersSmall + randomLettersBig,
|
|
58
|
+
'smallLetters': randomLettersSmall,
|
|
59
|
+
'bigLetters': randomLettersBig,
|
|
60
|
+
'small': randomLettersSmall + randomDigits,
|
|
61
|
+
'big': randomLettersBig + randomDigits,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const defaultRandomPathLength = 5
|
|
65
|
+
|
|
66
|
+
async function generateUrl(props, emit) {
|
|
67
|
+
console.log("GENERATE URL", props)
|
|
68
|
+
if(!props.targetType || !props.target) throw new Error("url must have target")
|
|
69
|
+
const prefix = props.prefix || ''
|
|
70
|
+
const suffix = props.suffix || ''
|
|
71
|
+
const randomCharacters = props.charset ? charsets[props.charset] : charsets.all
|
|
72
|
+
let randomPathLength = props.length || defaultRandomPathLength
|
|
73
|
+
const group = props.group
|
|
74
|
+
let maxLength = props.maxLength || 125
|
|
75
|
+
maxLength -= group.length
|
|
76
|
+
const sufixLength = 15
|
|
77
|
+
let path = ''
|
|
78
|
+
let random = false
|
|
79
|
+
if(props.path) {
|
|
80
|
+
path = path
|
|
81
|
+
const cutLength = maxLength - sufixLength/// because max id size
|
|
82
|
+
if(path.length > cutLength) {
|
|
83
|
+
let lastSep = path.lastIndexOf('-')
|
|
84
|
+
if(lastSep > cutLength - 40) path = path.slice(0, lastSep)
|
|
85
|
+
else path = path.slice(0, cutLength)
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
if(props.title) { // generated from title
|
|
89
|
+
path = createFromTitle(props.title)
|
|
90
|
+
const cutLength = maxLength - sufixLength /// because max id size
|
|
91
|
+
while(path.length > cutLength) {
|
|
92
|
+
let lastSep = path.lastIndexOf('-')
|
|
93
|
+
if(lastSep > cutLength - 40) path = path.slice(0, lastSep)
|
|
94
|
+
else path = path.slice(0, cutLength)
|
|
95
|
+
}
|
|
96
|
+
} else { // random
|
|
97
|
+
random = true
|
|
98
|
+
const charactersLength = randomCharacters.length
|
|
99
|
+
for(let i = 0; i < randomPathLength; i++) {
|
|
100
|
+
path += randomCharacters.charAt(Math.floor(Math.random() * charactersLength))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const basePath = path
|
|
105
|
+
|
|
106
|
+
let created = false
|
|
107
|
+
let conflict = false
|
|
108
|
+
do {
|
|
109
|
+
const fullPath = prefix + path + suffix
|
|
110
|
+
console.log("TRYING PATH", prefix + path + suffix)
|
|
111
|
+
const res = await UrlToTarget.rangeGet([props.targetType, props.domain, fullPath])
|
|
112
|
+
const count = res?.length ?? 0
|
|
113
|
+
if(count == 0) {
|
|
114
|
+
//Url.create({ id: `${group}_${path}`, group, path: prefix + path + suffix, to: props.to || null })
|
|
115
|
+
created = true
|
|
116
|
+
} else {
|
|
117
|
+
console.log("PATH TAKEN", prefix + path + suffix)
|
|
118
|
+
|
|
119
|
+
if(path.length >= maxLength) { /// because max id size
|
|
120
|
+
if(random) {
|
|
121
|
+
const charactersLength = randomCharacters.length
|
|
122
|
+
path = ''
|
|
123
|
+
for(let i = 0; i < randomPathLength; i++) {
|
|
124
|
+
path += randomCharacters.charAt(Math.floor(Math.random() * charactersLength))
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
path = basePath
|
|
128
|
+
}
|
|
129
|
+
const cutLength = maxLength - 10
|
|
130
|
+
if(path.length > cutLength) {
|
|
131
|
+
let lastSep = path.lastIndexOf('-')
|
|
132
|
+
if(lastSep > cutLength - 40) path = path.slice(0, lastSep)
|
|
133
|
+
else path = path.slice(0, cutLength)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if(!conflict) path += '-'
|
|
138
|
+
conflict = true
|
|
139
|
+
path += randomCharacters.charAt(Math.floor(Math.random() * randomCharacters.length))
|
|
140
|
+
}
|
|
141
|
+
} while(!created)
|
|
142
|
+
|
|
143
|
+
const fullPath = prefix + path + suffix
|
|
144
|
+
|
|
145
|
+
const canonicalId = App.encodeIdentifier([props.targetType, props.target])
|
|
146
|
+
const existingCanonical = await Canonical.get(canonicalId)
|
|
147
|
+
|
|
148
|
+
let url
|
|
149
|
+
if(props.redirect) {
|
|
150
|
+
url = app.generateUid()
|
|
151
|
+
emit({
|
|
152
|
+
type: 'targetOwnedRedirectCreated',
|
|
153
|
+
redirect: app.generateUid(),
|
|
154
|
+
identifiers: {
|
|
155
|
+
targetType: props.targetType,
|
|
156
|
+
target: props.target,
|
|
157
|
+
},
|
|
158
|
+
data: {
|
|
159
|
+
domain: props.domain,
|
|
160
|
+
path: fullPath
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
} else {
|
|
164
|
+
if(existingCanonical) {
|
|
165
|
+
emit({
|
|
166
|
+
type: 'targetOwnedRedirectCreated',
|
|
167
|
+
redirect: url,
|
|
168
|
+
identifiers: {
|
|
169
|
+
targetType: props.targetType,
|
|
170
|
+
target: props.target,
|
|
171
|
+
},
|
|
172
|
+
data: {
|
|
173
|
+
domain: existingCanonical.domain,
|
|
174
|
+
path: existingCanonical.path
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
emit({
|
|
179
|
+
type: 'targetOwnedCanonicalSet',
|
|
180
|
+
identifiers: {
|
|
181
|
+
targetType: props.targetType,
|
|
182
|
+
target: props.target
|
|
183
|
+
},
|
|
184
|
+
data: {
|
|
185
|
+
domain: props.domain,
|
|
186
|
+
path: fullPath
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
url = canonicalId
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
url,
|
|
194
|
+
domain: props.domain,
|
|
195
|
+
path: prefix + path + suffix
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
definition.trigger({
|
|
200
|
+
name: 'generateUrl',
|
|
201
|
+
properties: {
|
|
202
|
+
targetType: {
|
|
203
|
+
type: String,
|
|
204
|
+
validation: ['nonEmpty']
|
|
205
|
+
},
|
|
206
|
+
target: {
|
|
207
|
+
type: String,
|
|
208
|
+
validation: ['nonEmpty']
|
|
209
|
+
},
|
|
210
|
+
domain: {
|
|
211
|
+
type: String
|
|
212
|
+
},
|
|
213
|
+
title: {
|
|
214
|
+
type: String
|
|
215
|
+
},
|
|
216
|
+
path: {
|
|
217
|
+
type: String,
|
|
218
|
+
},
|
|
219
|
+
maxLength: {
|
|
220
|
+
type: Number
|
|
221
|
+
},
|
|
222
|
+
redirect: {
|
|
223
|
+
type: String
|
|
224
|
+
},
|
|
225
|
+
charset: {
|
|
226
|
+
type: String
|
|
227
|
+
},
|
|
228
|
+
prefix: {
|
|
229
|
+
type: String
|
|
230
|
+
},
|
|
231
|
+
suffix: {
|
|
232
|
+
type: String
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
waitForEvents: true,
|
|
236
|
+
queuedBy: 'targetType',
|
|
237
|
+
async execute (props, { client, service }, emit) {
|
|
238
|
+
return await generateUrl(props, emit)
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
definition.action({
|
|
243
|
+
name: 'generateUrl',
|
|
244
|
+
properties: {
|
|
245
|
+
targetType: {
|
|
246
|
+
type: String,
|
|
247
|
+
validation: ['nonEmpty']
|
|
248
|
+
},
|
|
249
|
+
target: {
|
|
250
|
+
type: String,
|
|
251
|
+
validation: ['nonEmpty']
|
|
252
|
+
},
|
|
253
|
+
domain: {
|
|
254
|
+
type: String
|
|
255
|
+
},
|
|
256
|
+
title: {
|
|
257
|
+
type: String
|
|
258
|
+
},
|
|
259
|
+
path: {
|
|
260
|
+
type: String,
|
|
261
|
+
},
|
|
262
|
+
maxLength: {
|
|
263
|
+
type: Number
|
|
264
|
+
},
|
|
265
|
+
redirect: {
|
|
266
|
+
type: String
|
|
267
|
+
},
|
|
268
|
+
charset: {
|
|
269
|
+
type: String
|
|
270
|
+
},
|
|
271
|
+
prefix: {
|
|
272
|
+
type: String
|
|
273
|
+
},
|
|
274
|
+
suffix: {
|
|
275
|
+
type: String
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
waitForEvents: true,
|
|
279
|
+
accessControl: {
|
|
280
|
+
roles: urlWriterRoles,
|
|
281
|
+
objects: ({ targetType: objectType, target: object }) => ({ objectType, object })
|
|
282
|
+
},
|
|
283
|
+
queuedBy: 'targetType',
|
|
284
|
+
async execute (props, { client, service }, emit) {
|
|
285
|
+
return await generateUrl(props, emit)
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
definition.action({
|
|
291
|
+
name: 'takeUrl',
|
|
292
|
+
waitForEvents: true,
|
|
293
|
+
properties: {
|
|
294
|
+
targetType: {
|
|
295
|
+
type: String,
|
|
296
|
+
validation: ['nonEmpty']
|
|
297
|
+
},
|
|
298
|
+
target: {
|
|
299
|
+
type: String,
|
|
300
|
+
validation: ['nonEmpty']
|
|
301
|
+
},
|
|
302
|
+
domain: {
|
|
303
|
+
type: String
|
|
304
|
+
},
|
|
305
|
+
path: {
|
|
306
|
+
type: String,
|
|
307
|
+
validation: ['nonEmpty']
|
|
308
|
+
},
|
|
309
|
+
redirect: {
|
|
310
|
+
type: String
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
accessControl: {
|
|
314
|
+
roles: urlWriterRoles,
|
|
315
|
+
objects: ({ targetType: objectType, target: object }) => ({ objectType, object })
|
|
316
|
+
},
|
|
317
|
+
queuedBy: 'targetType',
|
|
318
|
+
async execute({ targetType, target, domain, path, redirect }, { client, service }, emit) {
|
|
319
|
+
while(path[0] == '/') path = path.slice(1)
|
|
320
|
+
|
|
321
|
+
const res = await UrlToTarget.rangeGet([targetType, domain, path])
|
|
322
|
+
const count = res?.length ?? 0
|
|
323
|
+
if(count > 0) throw { properties: { path: "taken" } }
|
|
324
|
+
|
|
325
|
+
const canonicalId = App.encodeIdentifier([targetType, target])
|
|
326
|
+
const existingCanonical = await Canonical.get(canonicalId)
|
|
327
|
+
|
|
328
|
+
let url
|
|
329
|
+
if(redirect) {
|
|
330
|
+
url = app.generateUid()
|
|
331
|
+
emit({
|
|
332
|
+
type: 'targetOwnedCanonicalCreated',
|
|
333
|
+
redirect: url,
|
|
334
|
+
identifiers: {
|
|
335
|
+
targetType, target,
|
|
336
|
+
},
|
|
337
|
+
data: {
|
|
338
|
+
domain, path
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
} else {
|
|
342
|
+
if(existingCanonical) {
|
|
343
|
+
emit({
|
|
344
|
+
type: 'targetOwnedRedirectCreated',
|
|
345
|
+
redirect: app.generateUid(),
|
|
346
|
+
identifiers: {
|
|
347
|
+
targetType, target
|
|
348
|
+
},
|
|
349
|
+
data: {
|
|
350
|
+
domain: existingCanonical.domain,
|
|
351
|
+
path: existingCanonical.path
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
emit({
|
|
356
|
+
type: 'targetOwnedCanonicalSet',
|
|
357
|
+
identifiers: {
|
|
358
|
+
targetType, target,
|
|
359
|
+
},
|
|
360
|
+
data: {
|
|
361
|
+
domain, path
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
url = canonicalId
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return url
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
module.exports = definition
|
package/model.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const App = require("@live-change/framework")
|
|
2
|
+
const app = App.app()
|
|
3
|
+
const definition = require('./definition.js')
|
|
4
|
+
const config = definition.config
|
|
5
|
+
const {
|
|
6
|
+
urlReaderRoles = ['writer'],
|
|
7
|
+
urlWriterRoles = ['writer']
|
|
8
|
+
} = config
|
|
9
|
+
|
|
10
|
+
const urlProperties = {
|
|
11
|
+
domain: {
|
|
12
|
+
type: String
|
|
13
|
+
},
|
|
14
|
+
path: {
|
|
15
|
+
type: String,
|
|
16
|
+
validation: ['nonEmpty']
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Canonical = definition.model({
|
|
21
|
+
name: 'Canonical',
|
|
22
|
+
propertyOfAny: {
|
|
23
|
+
readAccessControl: { /// everyone can read canonical urls
|
|
24
|
+
roles: urlReaderRoles
|
|
25
|
+
},
|
|
26
|
+
resetAccessControl: {
|
|
27
|
+
roles: urlWriterRoles
|
|
28
|
+
},
|
|
29
|
+
to: 'target',
|
|
30
|
+
},
|
|
31
|
+
properties: {
|
|
32
|
+
...urlProperties
|
|
33
|
+
},
|
|
34
|
+
indexes: {
|
|
35
|
+
byUrl: {
|
|
36
|
+
property: ["targetType", "domain", "path"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const Redirect = definition.model({
|
|
42
|
+
name: 'Redirect',
|
|
43
|
+
itemOfAny: {
|
|
44
|
+
to: 'target',
|
|
45
|
+
readAccessControl: {
|
|
46
|
+
roles: urlReaderRoles
|
|
47
|
+
},
|
|
48
|
+
deleteAccessControl: {
|
|
49
|
+
roles: urlWriterRoles
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
properties: {
|
|
53
|
+
...urlProperties
|
|
54
|
+
},
|
|
55
|
+
indexes: {
|
|
56
|
+
byUrl: {
|
|
57
|
+
property: ["targetType", "domain", "path"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const UrlToTarget = definition.index({
|
|
63
|
+
name: 'Urls',
|
|
64
|
+
function: async function(input, output) {
|
|
65
|
+
const urlMapper = urlType => ({targetType, domain, path, target}) =>
|
|
66
|
+
({ id: `"${targetType}":"${domain}":${JSON.stringify(path)}:"${urlType}"_"${target}"`, target, urlType })
|
|
67
|
+
const redirectMapper = urlMapper('redirect')
|
|
68
|
+
const canonicalMapper = urlMapper('canonical')
|
|
69
|
+
await input.table('url_Redirect').onChange(
|
|
70
|
+
(obj, oldObj) => output.change(obj && redirectMapper(obj), oldObj && redirectMapper(oldObj))
|
|
71
|
+
)
|
|
72
|
+
await input.table('url_Canonical').onChange(
|
|
73
|
+
(obj, oldObj) => output.change(obj && canonicalMapper(obj), oldObj && canonicalMapper(oldObj))
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
module.exports = { Canonical, Redirect, UrlToTarget }
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@live-change/url-service",
|
|
3
|
+
"version": "0.2.45",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "NODE_ENV=test tape tests/*"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/live-change/live-change-services.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/live-change/live-change-services/issues"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/live-change/live-change-services",
|
|
18
|
+
"author": {
|
|
19
|
+
"email": "michal@laszczewski.pl",
|
|
20
|
+
"name": "Michał Łaszczewski",
|
|
21
|
+
"url": "https://www.viamage.com/"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@live-change/framework": "0.6.11",
|
|
25
|
+
"@live-change/relations-plugin": "0.6.11",
|
|
26
|
+
"lru-cache": "^7.12.0",
|
|
27
|
+
"pluralize": "8.0.0",
|
|
28
|
+
"progress-stream": "^2.0.0",
|
|
29
|
+
"prosemirror-model": "^1.18.1"
|
|
30
|
+
},
|
|
31
|
+
"gitHead": "aab9b222492335dda687dcf4a1c2794ec4fa610f"
|
|
32
|
+
}
|