@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.
Files changed (4) hide show
  1. package/definition.js +12 -0
  2. package/index.js +372 -0
  3. package/model.js +78 -0
  4. 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
+ }