@graffiti-garden/implementation-local 0.4.3 → 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/dist/browser/index.js +2 -2
- package/dist/browser/index.js.map +3 -3
- package/dist/cjs/database.js +474 -1
- package/dist/cjs/database.js.map +3 -3
- package/dist/cjs/index.js +50 -1
- package/dist/cjs/index.js.map +3 -3
- package/dist/cjs/session-manager.js +105 -2
- package/dist/cjs/session-manager.js.map +2 -2
- package/dist/cjs/tests.spec.js +10 -1
- package/dist/cjs/tests.spec.js.map +3 -3
- package/dist/cjs/utilities.js +79 -1
- package/dist/cjs/utilities.js.map +3 -3
- package/dist/database.d.ts +14 -10
- package/dist/database.d.ts.map +1 -1
- package/dist/esm/database.js +457 -1
- package/dist/esm/database.js.map +3 -3
- package/dist/esm/index.js +32 -1
- package/dist/esm/index.js.map +3 -3
- package/dist/esm/session-manager.js +85 -2
- package/dist/esm/session-manager.js.map +2 -2
- package/dist/esm/tests.spec.js +14 -1
- package/dist/esm/tests.spec.js.map +3 -3
- package/dist/esm/utilities.js +64 -1
- package/dist/esm/utilities.js.map +3 -3
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/utilities.d.ts +2 -7
- package/dist/utilities.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/database.ts +107 -50
- package/src/index.ts +0 -4
- package/src/tests.spec.ts +0 -2
- package/src/utilities.ts +5 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graffiti-garden/implementation-local",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A local implementation of the Graffiti API using PouchDB",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"url": "https://github.com/graffiti-garden/implementation-local/issues"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
+
"@vitest/coverage-v8": "^3.0.6",
|
|
76
77
|
"esbuild": "^0.25.0",
|
|
77
78
|
"esbuild-plugin-polyfill-node": "^0.3.0",
|
|
78
79
|
"tsx": "^4.19.2",
|
|
@@ -80,7 +81,7 @@
|
|
|
80
81
|
"vitest": "^3.0.5"
|
|
81
82
|
},
|
|
82
83
|
"dependencies": {
|
|
83
|
-
"@graffiti-garden/api": "^0.
|
|
84
|
+
"@graffiti-garden/api": "^0.5.0",
|
|
84
85
|
"@repeaterjs/repeater": "^3.0.6",
|
|
85
86
|
"@types/pouchdb": "^6.4.2",
|
|
86
87
|
"ajv": "^8.17.1",
|
package/src/database.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
GraffitiObjectBase,
|
|
4
4
|
GraffitiLocation,
|
|
5
5
|
JSONSchema,
|
|
6
|
+
GraffitiSession,
|
|
6
7
|
} from "@graffiti-garden/api";
|
|
7
8
|
import {
|
|
8
9
|
GraffitiErrorNotFound,
|
|
@@ -11,14 +12,13 @@ import {
|
|
|
11
12
|
GraffitiErrorPatchError,
|
|
12
13
|
} from "@graffiti-garden/api";
|
|
13
14
|
import {
|
|
14
|
-
locationToUri,
|
|
15
|
-
unpackLocationOrUri,
|
|
16
15
|
randomBase64,
|
|
17
16
|
applyGraffitiPatch,
|
|
18
17
|
maskGraffitiObject,
|
|
19
18
|
isActorAllowedGraffitiObject,
|
|
20
19
|
isObjectNewer,
|
|
21
20
|
compileGraffitiObjectSchema,
|
|
21
|
+
unpackLocationOrUri,
|
|
22
22
|
} from "./utilities.js";
|
|
23
23
|
import { Repeater } from "@repeaterjs/repeater";
|
|
24
24
|
import type Ajv from "ajv";
|
|
@@ -37,19 +37,19 @@ export interface GraffitiLocalOptions {
|
|
|
37
37
|
*/
|
|
38
38
|
pouchDBOptions?: PouchDB.Configuration.DatabaseConfiguration;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* Defaults to `"local"`.
|
|
40
|
+
* Includes the scheme and other information (possibly domain name)
|
|
41
|
+
* to prefix prefixes all URIs put in the system. Defaults to `graffiti:local`.
|
|
43
42
|
*/
|
|
44
|
-
|
|
43
|
+
origin?: string;
|
|
45
44
|
/**
|
|
46
|
-
* Whether to allow putting objects
|
|
47
|
-
*
|
|
45
|
+
* Whether to allow putting objects at arbtirary URIs, i.e.
|
|
46
|
+
* URIs that are *not* prefixed with the origin or not generated
|
|
47
|
+
* by the system. Defaults to `false`.
|
|
48
48
|
*
|
|
49
49
|
* Allows this implementation to be used as a client-side cache
|
|
50
50
|
* for remote sources.
|
|
51
51
|
*/
|
|
52
|
-
|
|
52
|
+
allowSettingArbitraryUris?: boolean;
|
|
53
53
|
/**
|
|
54
54
|
* Whether to allow the user to set the lastModified field
|
|
55
55
|
* when putting objects. Defaults to `false`.
|
|
@@ -72,7 +72,7 @@ export interface GraffitiLocalOptions {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
const DEFAULT_TOMBSTONE_RETENTION = 86400000; // 1 day in milliseconds
|
|
75
|
-
const
|
|
75
|
+
const DEFAULT_ORIGIN = "graffiti:local:";
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* An implementation of only the database operations of the
|
|
@@ -95,6 +95,7 @@ export class GraffitiLocalDatabase
|
|
|
95
95
|
protected applyPatch_: Promise<typeof applyPatch> | undefined;
|
|
96
96
|
protected ajv_: Promise<Ajv> | undefined;
|
|
97
97
|
protected readonly options: GraffitiLocalOptions;
|
|
98
|
+
protected readonly origin: string;
|
|
98
99
|
|
|
99
100
|
get db() {
|
|
100
101
|
if (!this.db_) {
|
|
@@ -201,10 +202,14 @@ export class GraffitiLocalDatabase
|
|
|
201
202
|
|
|
202
203
|
constructor(options?: GraffitiLocalOptions) {
|
|
203
204
|
this.options = options ?? {};
|
|
205
|
+
this.origin = this.options.origin ?? DEFAULT_ORIGIN;
|
|
206
|
+
if (!this.origin.endsWith(":") && !this.origin.endsWith("/")) {
|
|
207
|
+
this.origin += "/";
|
|
208
|
+
}
|
|
204
209
|
}
|
|
205
210
|
|
|
206
|
-
protected async
|
|
207
|
-
const uri =
|
|
211
|
+
protected async allDocsAtLocation(locationOrUri: GraffitiLocation | string) {
|
|
212
|
+
const uri = unpackLocationOrUri(locationOrUri) + "/";
|
|
208
213
|
const results = await (
|
|
209
214
|
await this.db
|
|
210
215
|
).allDocs({
|
|
@@ -227,20 +232,22 @@ export class GraffitiLocalDatabase
|
|
|
227
232
|
}
|
|
228
233
|
|
|
229
234
|
protected docId(location: GraffitiLocation) {
|
|
230
|
-
return
|
|
235
|
+
return location.uri + "/" + randomBase64();
|
|
231
236
|
}
|
|
232
237
|
|
|
233
238
|
get: Graffiti["get"] = async (...args) => {
|
|
234
239
|
const [locationOrUri, schema, session] = args;
|
|
235
|
-
const { location } = unpackLocationOrUri(locationOrUri);
|
|
236
240
|
|
|
237
|
-
const docsAll = await this.
|
|
241
|
+
const docsAll = await this.allDocsAtLocation(locationOrUri);
|
|
238
242
|
|
|
239
243
|
// Filter out ones not allowed
|
|
240
244
|
const docs = docsAll.filter((doc) =>
|
|
241
245
|
isActorAllowedGraffitiObject(doc, session),
|
|
242
246
|
);
|
|
243
|
-
if (!docs.length)
|
|
247
|
+
if (!docs.length)
|
|
248
|
+
throw new GraffitiErrorNotFound(
|
|
249
|
+
"The object you are trying to get either does not exist or you are not allowed to see it",
|
|
250
|
+
);
|
|
244
251
|
|
|
245
252
|
// Get the most recent document
|
|
246
253
|
const doc = docs.reduce((a, b) => (isObjectNewer(a, b) ? a : b));
|
|
@@ -268,11 +275,35 @@ export class GraffitiLocalDatabase
|
|
|
268
275
|
* spared.
|
|
269
276
|
*/
|
|
270
277
|
protected async deleteAtLocation(
|
|
271
|
-
|
|
272
|
-
|
|
278
|
+
locationOrUri: GraffitiLocation | string,
|
|
279
|
+
options: {
|
|
280
|
+
keepLatest?: boolean;
|
|
281
|
+
session?: GraffitiSession;
|
|
282
|
+
} = {
|
|
283
|
+
keepLatest: false,
|
|
284
|
+
},
|
|
273
285
|
) {
|
|
274
|
-
const docsAtLocationAll = await this.
|
|
275
|
-
const
|
|
286
|
+
const docsAtLocationAll = await this.allDocsAtLocation(locationOrUri);
|
|
287
|
+
const docsAtLocationAllowed = options.session
|
|
288
|
+
? docsAtLocationAll.filter((doc) =>
|
|
289
|
+
isActorAllowedGraffitiObject(doc, options.session),
|
|
290
|
+
)
|
|
291
|
+
: docsAtLocationAll;
|
|
292
|
+
if (!docsAtLocationAllowed.length) {
|
|
293
|
+
throw new GraffitiErrorNotFound(
|
|
294
|
+
"The object you are trying to delete either does not exist or you are not allowed to see it",
|
|
295
|
+
);
|
|
296
|
+
} else if (
|
|
297
|
+
options.session &&
|
|
298
|
+
docsAtLocationAllowed.some((doc) => doc.actor !== options.session?.actor)
|
|
299
|
+
) {
|
|
300
|
+
throw new GraffitiErrorForbidden(
|
|
301
|
+
"You cannot delete an object owned by another actor",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const docsAtLocation = docsAtLocationAllowed.filter(
|
|
305
|
+
(doc) => !doc.tombstone,
|
|
306
|
+
);
|
|
276
307
|
if (!docsAtLocation.length) return undefined;
|
|
277
308
|
|
|
278
309
|
// Get the most recent lastModified timestamp.
|
|
@@ -282,14 +313,14 @@ export class GraffitiLocalDatabase
|
|
|
282
313
|
|
|
283
314
|
// Delete all old docs
|
|
284
315
|
const docsToDelete = docsAtLocation.filter(
|
|
285
|
-
(doc) => !keepLatest || doc.lastModified < latestModified,
|
|
316
|
+
(doc) => !options.keepLatest || doc.lastModified < latestModified,
|
|
286
317
|
);
|
|
287
318
|
|
|
288
319
|
// For docs with the same timestamp,
|
|
289
320
|
// keep the one with the highest _id
|
|
290
321
|
// to break concurrency ties
|
|
291
322
|
const concurrentDocsAll = docsAtLocation.filter(
|
|
292
|
-
(doc) => keepLatest && doc.lastModified === latestModified,
|
|
323
|
+
(doc) => options.keepLatest && doc.lastModified === latestModified,
|
|
293
324
|
);
|
|
294
325
|
if (concurrentDocsAll.length) {
|
|
295
326
|
const keepDocId = concurrentDocsAll
|
|
@@ -301,7 +332,9 @@ export class GraffitiLocalDatabase
|
|
|
301
332
|
docsToDelete.push(...concurrentDocsToDelete);
|
|
302
333
|
}
|
|
303
334
|
|
|
304
|
-
const lastModified = keepLatest
|
|
335
|
+
const lastModified = options.keepLatest
|
|
336
|
+
? latestModified
|
|
337
|
+
: new Date().getTime();
|
|
305
338
|
|
|
306
339
|
const deleteResults = await (
|
|
307
340
|
await this.db
|
|
@@ -336,14 +369,11 @@ export class GraffitiLocalDatabase
|
|
|
336
369
|
|
|
337
370
|
delete: Graffiti["delete"] = async (...args) => {
|
|
338
371
|
const [locationOrUri, session] = args;
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const deletedObject = await this.deleteAtLocation(location);
|
|
372
|
+
const deletedObject = await this.deleteAtLocation(locationOrUri, {
|
|
373
|
+
session,
|
|
374
|
+
});
|
|
345
375
|
if (!deletedObject) {
|
|
346
|
-
throw new GraffitiErrorNotFound();
|
|
376
|
+
throw new GraffitiErrorNotFound("The object has already been deleted");
|
|
347
377
|
}
|
|
348
378
|
return deletedObject;
|
|
349
379
|
};
|
|
@@ -351,19 +381,33 @@ export class GraffitiLocalDatabase
|
|
|
351
381
|
put: Graffiti["put"] = async (...args) => {
|
|
352
382
|
const [objectPartial, session] = args;
|
|
353
383
|
if (objectPartial.actor && objectPartial.actor !== session.actor) {
|
|
354
|
-
throw new GraffitiErrorForbidden();
|
|
355
|
-
}
|
|
356
|
-
if (
|
|
357
|
-
objectPartial.source &&
|
|
358
|
-
objectPartial.source !==
|
|
359
|
-
(this.options.sourceName ?? DEFAULT_SOURCE_NAME) &&
|
|
360
|
-
!(this.options.allowOtherSources ?? false)
|
|
361
|
-
) {
|
|
362
384
|
throw new GraffitiErrorForbidden(
|
|
363
|
-
"
|
|
385
|
+
"Cannot put an object with a different actor than the session actor",
|
|
364
386
|
);
|
|
365
387
|
}
|
|
366
388
|
|
|
389
|
+
if (objectPartial.uri) {
|
|
390
|
+
let oldObject: GraffitiObjectBase | undefined;
|
|
391
|
+
try {
|
|
392
|
+
oldObject = await this.get(objectPartial.uri, {}, session);
|
|
393
|
+
} catch (e) {
|
|
394
|
+
if (e instanceof GraffitiErrorNotFound) {
|
|
395
|
+
if (!this.options.allowSettingArbitraryUris) {
|
|
396
|
+
throw new GraffitiErrorNotFound(
|
|
397
|
+
"The object you are trying to replace does not exist or you are not allowed to see it",
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
throw e;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (oldObject?.actor !== session.actor) {
|
|
405
|
+
throw new GraffitiErrorForbidden(
|
|
406
|
+
"The object you are trying to replace is owned by another actor",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
367
411
|
const lastModified =
|
|
368
412
|
((this.options.allowSettinngLastModified ?? false) &&
|
|
369
413
|
objectPartial.lastModified) ||
|
|
@@ -373,9 +417,7 @@ export class GraffitiLocalDatabase
|
|
|
373
417
|
value: objectPartial.value,
|
|
374
418
|
channels: objectPartial.channels,
|
|
375
419
|
allowed: objectPartial.allowed,
|
|
376
|
-
|
|
377
|
-
source:
|
|
378
|
-
objectPartial.source ?? this.options.sourceName ?? DEFAULT_SOURCE_NAME,
|
|
420
|
+
uri: objectPartial.uri ?? this.origin + randomBase64(),
|
|
379
421
|
actor: session.actor,
|
|
380
422
|
tombstone: false,
|
|
381
423
|
lastModified,
|
|
@@ -389,7 +431,9 @@ export class GraffitiLocalDatabase
|
|
|
389
431
|
});
|
|
390
432
|
|
|
391
433
|
// Delete the old object
|
|
392
|
-
const previousObject = await this.deleteAtLocation(object,
|
|
434
|
+
const previousObject = await this.deleteAtLocation(object, {
|
|
435
|
+
keepLatest: true,
|
|
436
|
+
});
|
|
393
437
|
if (previousObject) {
|
|
394
438
|
return previousObject;
|
|
395
439
|
} else {
|
|
@@ -397,7 +441,7 @@ export class GraffitiLocalDatabase
|
|
|
397
441
|
...object,
|
|
398
442
|
value: {},
|
|
399
443
|
channels: [],
|
|
400
|
-
allowed:
|
|
444
|
+
allowed: [],
|
|
401
445
|
tombstone: true,
|
|
402
446
|
};
|
|
403
447
|
}
|
|
@@ -405,12 +449,23 @@ export class GraffitiLocalDatabase
|
|
|
405
449
|
|
|
406
450
|
patch: Graffiti["patch"] = async (...args) => {
|
|
407
451
|
const [patch, locationOrUri, session] = args;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
452
|
+
let originalObject: GraffitiObjectBase;
|
|
453
|
+
try {
|
|
454
|
+
originalObject = await this.get(locationOrUri, {}, session);
|
|
455
|
+
} catch (e) {
|
|
456
|
+
if (e instanceof GraffitiErrorNotFound) {
|
|
457
|
+
throw new GraffitiErrorNotFound(
|
|
458
|
+
"The object you are trying to patch does not exist or you are not allowed to see it",
|
|
459
|
+
);
|
|
460
|
+
} else {
|
|
461
|
+
throw e;
|
|
462
|
+
}
|
|
411
463
|
}
|
|
412
|
-
|
|
413
|
-
|
|
464
|
+
if (originalObject.actor !== session.actor) {
|
|
465
|
+
throw new GraffitiErrorForbidden(
|
|
466
|
+
"The object you are trying to patch is owned by another actor",
|
|
467
|
+
);
|
|
468
|
+
} else if (originalObject.tombstone) {
|
|
414
469
|
throw new GraffitiErrorNotFound(
|
|
415
470
|
"The object you are trying to patch has been deleted",
|
|
416
471
|
);
|
|
@@ -461,7 +516,9 @@ export class GraffitiLocalDatabase
|
|
|
461
516
|
});
|
|
462
517
|
|
|
463
518
|
// Delete the old object
|
|
464
|
-
await this.deleteAtLocation(patchObject,
|
|
519
|
+
await this.deleteAtLocation(patchObject, {
|
|
520
|
+
keepLatest: true,
|
|
521
|
+
});
|
|
465
522
|
|
|
466
523
|
return {
|
|
467
524
|
...originalObject,
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
GraffitiLocalDatabase,
|
|
5
5
|
type GraffitiLocalOptions,
|
|
6
6
|
} from "./database.js";
|
|
7
|
-
import { locationToUri, uriToLocation } from "./utilities.js";
|
|
8
7
|
|
|
9
8
|
export type { GraffitiLocalOptions };
|
|
10
9
|
|
|
@@ -16,9 +15,6 @@ export type { GraffitiLocalOptions };
|
|
|
16
15
|
* although using it with a remote server will not be secure.
|
|
17
16
|
*/
|
|
18
17
|
export class GraffitiLocal extends Graffiti {
|
|
19
|
-
locationToUri = locationToUri;
|
|
20
|
-
uriToLocation = uriToLocation;
|
|
21
|
-
|
|
22
18
|
protected sessionManagerLocal = new GraffitiLocalSessionManager();
|
|
23
19
|
login = this.sessionManagerLocal.login.bind(this.sessionManagerLocal);
|
|
24
20
|
logout = this.sessionManagerLocal.logout.bind(this.sessionManagerLocal);
|
package/src/tests.spec.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
graffitiLocationTests,
|
|
3
2
|
graffitiCRUDTests,
|
|
4
3
|
graffitiDiscoverTests,
|
|
5
4
|
graffitiOrphanTests,
|
|
@@ -11,7 +10,6 @@ const useGraffiti = () => new GraffitiLocal();
|
|
|
11
10
|
const useSession1 = () => ({ actor: "someone" });
|
|
12
11
|
const useSession2 = () => ({ actor: "someoneelse" });
|
|
13
12
|
|
|
14
|
-
graffitiLocationTests(useGraffiti);
|
|
15
13
|
graffitiCRUDTests(useGraffiti, useSession1, useSession2);
|
|
16
14
|
graffitiDiscoverTests(useGraffiti, useSession1, useSession2);
|
|
17
15
|
graffitiOrphanTests(useGraffiti, useSession1, useSession2);
|
package/src/utilities.ts
CHANGED
|
@@ -5,36 +5,21 @@ import {
|
|
|
5
5
|
GraffitiErrorPatchTestFailed,
|
|
6
6
|
} from "@graffiti-garden/api";
|
|
7
7
|
import type {
|
|
8
|
-
Graffiti,
|
|
9
8
|
GraffitiObject,
|
|
10
9
|
GraffitiObjectBase,
|
|
11
|
-
GraffitiLocation,
|
|
12
10
|
GraffitiPatch,
|
|
13
11
|
JSONSchema,
|
|
14
12
|
GraffitiSession,
|
|
13
|
+
GraffitiLocation,
|
|
15
14
|
} from "@graffiti-garden/api";
|
|
16
15
|
import type { Ajv } from "ajv";
|
|
17
16
|
import type { applyPatch } from "fast-json-patch";
|
|
18
17
|
|
|
19
|
-
export
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const uriToLocation: Graffiti["uriToLocation"] = (uri) => {
|
|
24
|
-
const parts = uri.split("/");
|
|
25
|
-
const nameEncoded = parts.pop();
|
|
26
|
-
const webIdEncoded = parts.pop();
|
|
27
|
-
if (!nameEncoded || !webIdEncoded || !parts.length) {
|
|
28
|
-
throw new GraffitiErrorInvalidUri();
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
name: decodeURIComponent(nameEncoded),
|
|
32
|
-
actor: decodeURIComponent(webIdEncoded),
|
|
33
|
-
source: parts.join("/"),
|
|
34
|
-
};
|
|
35
|
-
};
|
|
18
|
+
export function unpackLocationOrUri(locationOrUri: GraffitiLocation | string) {
|
|
19
|
+
return typeof locationOrUri === "string" ? locationOrUri : locationOrUri.uri;
|
|
20
|
+
}
|
|
36
21
|
|
|
37
|
-
export function randomBase64(numBytes: number =
|
|
22
|
+
export function randomBase64(numBytes: number = 24) {
|
|
38
23
|
const bytes = new Uint8Array(numBytes);
|
|
39
24
|
crypto.getRandomValues(bytes);
|
|
40
25
|
// Convert it to base64
|
|
@@ -43,24 +28,6 @@ export function randomBase64(numBytes: number = 16) {
|
|
|
43
28
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/\=+$/, "");
|
|
44
29
|
}
|
|
45
30
|
|
|
46
|
-
export function unpackLocationOrUri(locationOrUri: GraffitiLocation | string) {
|
|
47
|
-
if (typeof locationOrUri === "string") {
|
|
48
|
-
return {
|
|
49
|
-
location: uriToLocation(locationOrUri),
|
|
50
|
-
uri: locationOrUri,
|
|
51
|
-
};
|
|
52
|
-
} else {
|
|
53
|
-
return {
|
|
54
|
-
location: {
|
|
55
|
-
name: locationOrUri.name,
|
|
56
|
-
actor: locationOrUri.actor,
|
|
57
|
-
source: locationOrUri.source,
|
|
58
|
-
},
|
|
59
|
-
uri: locationToUri(locationOrUri),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
31
|
export function isObjectNewer(
|
|
65
32
|
left: GraffitiObjectBase,
|
|
66
33
|
right: GraffitiObjectBase,
|