@verdant-web/common 2.3.4 → 2.4.0-next.1
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/esm/authz.d.ts +25 -0
- package/dist/esm/authz.js +61 -0
- package/dist/esm/authz.js.map +1 -0
- package/dist/esm/baseline.d.ts +1 -0
- package/dist/esm/baseline.js +3 -0
- package/dist/esm/baseline.js.map +1 -1
- package/dist/esm/files.d.ts +4 -0
- package/dist/esm/files.js +18 -0
- package/dist/esm/files.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes.js +1 -1
- package/dist/esm/indexes.js.map +1 -1
- package/dist/esm/indexes.test.js +1 -1
- package/dist/esm/indexes.test.js.map +1 -1
- package/dist/esm/migration.d.ts +4 -2
- package/dist/esm/migration.js.map +1 -1
- package/dist/esm/oids.d.ts +6 -24
- package/dist/esm/oids.js +18 -107
- package/dist/esm/oids.js.map +1 -1
- package/dist/esm/oids.test.js +1 -188
- package/dist/esm/oids.test.js.map +1 -1
- package/dist/esm/oidsLegacy.d.ts +18 -0
- package/dist/esm/oidsLegacy.js +72 -0
- package/dist/esm/oidsLegacy.js.map +1 -0
- package/dist/esm/oidsLegacy.test.d.ts +1 -0
- package/dist/esm/oidsLegacy.test.js +115 -0
- package/dist/esm/oidsLegacy.test.js.map +1 -0
- package/dist/esm/operation.d.ts +28 -4
- package/dist/esm/operation.js +70 -35
- package/dist/esm/operation.js.map +1 -1
- package/dist/esm/operation.test.js.map +1 -1
- package/dist/esm/presence.d.ts +0 -2
- package/dist/esm/schema/fields.js +1 -1
- package/dist/esm/schema/fields.js.map +1 -1
- package/dist/esm/schema/validation.js +14 -7
- package/dist/esm/schema/validation.js.map +1 -1
- package/package.json +1 -1
- package/src/authz.ts +74 -0
- package/src/baseline.ts +5 -4
- package/src/files.ts +25 -0
- package/src/index.ts +2 -0
- package/src/indexes.test.ts +1 -1
- package/src/indexes.ts +1 -1
- package/src/migration.ts +5 -1
- package/src/oids.test.ts +0 -241
- package/src/oids.ts +24 -122
- package/src/oidsLegacy.test.ts +147 -0
- package/src/oidsLegacy.ts +87 -0
- package/src/operation.test.ts +0 -1
- package/src/operation.ts +202 -103
- package/src/presence.ts +0 -2
- package/src/schema/fields.ts +1 -1
- package/src/schema/validation.ts +23 -14
package/src/authz.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { DocumentBaseline } from './baseline.js';
|
|
2
|
+
import { Operation } from './operation.js';
|
|
3
|
+
|
|
4
|
+
function encode(str: string): AuthorizationKey {
|
|
5
|
+
if (typeof Buffer !== 'undefined') {
|
|
6
|
+
const val = Buffer.from(str).toString('base64');
|
|
7
|
+
return val as AuthorizationKey;
|
|
8
|
+
}
|
|
9
|
+
const val = btoa(str);
|
|
10
|
+
return val as AuthorizationKey;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function decode(str: string): string {
|
|
14
|
+
if (typeof Buffer !== 'undefined') {
|
|
15
|
+
return Buffer.from(str, 'base64').toString();
|
|
16
|
+
}
|
|
17
|
+
return atob(str);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type AuthorizationKey = string & {
|
|
21
|
+
'@@type': 'authz';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const authz = {
|
|
25
|
+
onlyUser: (userId: string): AuthorizationKey => encode(`u:${userId}:*`),
|
|
26
|
+
onlyMe: (): AuthorizationKey => authz.onlyUser(ORIGINATOR_SUBJECT),
|
|
27
|
+
decode: (encoded: string) => {
|
|
28
|
+
const decoded = decode(encoded);
|
|
29
|
+
const parts = decoded.split(':');
|
|
30
|
+
if (parts.length !== 3) {
|
|
31
|
+
throw new Error('Invalid authz string');
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
scope: parts[0],
|
|
35
|
+
subject: parts[1],
|
|
36
|
+
action: parts[2],
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const ORIGINATOR_SUBJECT = '$$_originator_$$';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Rewrites the special "originator" constant subject (used by
|
|
45
|
+
* local-only clients) to the given user ID.
|
|
46
|
+
*
|
|
47
|
+
* Used to initialize a library from a local-only replica
|
|
48
|
+
*/
|
|
49
|
+
export function rewriteAuthzOriginator(
|
|
50
|
+
data: { operations?: Operation[]; baselines?: DocumentBaseline[] },
|
|
51
|
+
newSubject: string,
|
|
52
|
+
) {
|
|
53
|
+
const { operations, baselines } = data;
|
|
54
|
+
if (operations) {
|
|
55
|
+
for (const op of operations) {
|
|
56
|
+
if (op.authz) {
|
|
57
|
+
const decoded = authz.decode(op.authz);
|
|
58
|
+
if (decoded.subject === ORIGINATOR_SUBJECT) {
|
|
59
|
+
op.authz = authz.onlyUser(newSubject);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (baselines) {
|
|
65
|
+
for (const baseline of baselines) {
|
|
66
|
+
if (baseline.authz) {
|
|
67
|
+
const decoded = authz.decode(baseline.authz);
|
|
68
|
+
if (decoded.subject === ORIGINATOR_SUBJECT) {
|
|
69
|
+
baseline.authz = authz.onlyUser(newSubject);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/baseline.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
// A: Docs without a base state don't have a baseline, it's
|
|
2
|
+
// written upon rebasing. If you apply ops to an undefined
|
|
3
|
+
// snapshot without an initialize, it remains undefined.
|
|
4
|
+
|
|
1
5
|
export type DocumentBaseline<T extends any = any> = {
|
|
2
6
|
oid: string;
|
|
3
7
|
snapshot: T;
|
|
4
8
|
timestamp: string;
|
|
5
|
-
|
|
6
|
-
// a document which was snapshotted in a deleted state, vs
|
|
7
|
-
// one without a base state at all? what happens if you apply
|
|
8
|
-
// ops onto an undefined snapshot (which aren't initialize)?
|
|
9
|
+
authz?: string;
|
|
9
10
|
};
|
package/src/files.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isObject } from './utils.js';
|
|
2
|
+
|
|
1
3
|
export type FileRef = {
|
|
2
4
|
'@@type': 'file';
|
|
3
5
|
id: string;
|
|
@@ -22,7 +24,9 @@ export type FileData = {
|
|
|
22
24
|
remote: boolean;
|
|
23
25
|
name: string;
|
|
24
26
|
type: string;
|
|
27
|
+
/** A local File instance, if available. */
|
|
25
28
|
file?: Blob;
|
|
29
|
+
/** The server URL of this file. */
|
|
26
30
|
url?: string;
|
|
27
31
|
};
|
|
28
32
|
|
|
@@ -32,3 +36,24 @@ export function getAllFileFields(snapshot: any): [string, FileRef][] {
|
|
|
32
36
|
FileRef,
|
|
33
37
|
][];
|
|
34
38
|
}
|
|
39
|
+
|
|
40
|
+
export function isFile(value: any) {
|
|
41
|
+
if (typeof File !== 'undefined' && value instanceof File) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (typeof Blob !== 'undefined' && value instanceof Blob) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isFileData(value: any): value is FileData {
|
|
51
|
+
return (
|
|
52
|
+
value &&
|
|
53
|
+
isObject(value) &&
|
|
54
|
+
typeof value.id === 'string' &&
|
|
55
|
+
typeof value.remote === 'boolean' &&
|
|
56
|
+
typeof value.name === 'string' &&
|
|
57
|
+
typeof value.type === 'string'
|
|
58
|
+
);
|
|
59
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type {
|
|
|
20
20
|
export type { UserInfo } from './presence.js';
|
|
21
21
|
export * from './patch.js';
|
|
22
22
|
export * from './oids.js';
|
|
23
|
+
export * from './oidsLegacy.js';
|
|
23
24
|
export * from './EventSubscriber.js';
|
|
24
25
|
export * from './undo.js';
|
|
25
26
|
export * from './batching.js';
|
|
@@ -28,3 +29,4 @@ export type { Ref } from './refs.js';
|
|
|
28
29
|
export { makeObjectRef, makeFileRef, isRef, compareRefs } from './refs.js';
|
|
29
30
|
export * from './memo.js';
|
|
30
31
|
export * from './error.js';
|
|
32
|
+
export * from './authz.js';
|
package/src/indexes.test.ts
CHANGED
package/src/indexes.ts
CHANGED
package/src/migration.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getOid,
|
|
14
14
|
hasDefault,
|
|
15
15
|
validateEntity,
|
|
16
|
+
AuthorizationKey,
|
|
16
17
|
} from './index.js';
|
|
17
18
|
|
|
18
19
|
/**@deprecated */
|
|
@@ -473,7 +474,10 @@ type MigrationQueries<Old extends SchemaDocuments> = {
|
|
|
473
474
|
};
|
|
474
475
|
type MigrationMutations<New extends SchemaDocuments> = {
|
|
475
476
|
[Key in keyof New]: {
|
|
476
|
-
put(
|
|
477
|
+
put(
|
|
478
|
+
document: New[Key]['init'],
|
|
479
|
+
options?: { access?: AuthorizationKey },
|
|
480
|
+
): Promise<New[Key]['snapshot']>;
|
|
477
481
|
delete(primaryKey: string): Promise<void>;
|
|
478
482
|
};
|
|
479
483
|
};
|
package/src/oids.test.ts
CHANGED
|
@@ -1,27 +1,15 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { createFileRef } from './files.js';
|
|
3
3
|
import {
|
|
4
|
-
areOidsRelated,
|
|
5
4
|
assignOid,
|
|
6
|
-
assignOidPropertiesToAllSubObjects,
|
|
7
|
-
assignOidProperty,
|
|
8
5
|
assignOidsToAllSubObjects,
|
|
9
6
|
createOid,
|
|
10
|
-
createSubOid,
|
|
11
7
|
decomposeOid,
|
|
12
|
-
getOid,
|
|
13
8
|
getOidSubIdRange,
|
|
14
|
-
getOidRoot,
|
|
15
9
|
hasOid,
|
|
16
|
-
maybeGetOidProperty,
|
|
17
10
|
normalize,
|
|
18
11
|
normalizeFirstLevel,
|
|
19
12
|
ObjectIdentifier,
|
|
20
|
-
removeOidPropertiesFromAllSubObjects,
|
|
21
|
-
getLegacyDotOidSubIdRange,
|
|
22
|
-
replaceLegacyOidsInJsonString,
|
|
23
|
-
MATCH_LEGACY_OID_JSON_STRING,
|
|
24
|
-
replaceLegacyOidsInObject,
|
|
25
13
|
} from './oids.js';
|
|
26
14
|
|
|
27
15
|
describe('normalizing an object', () => {
|
|
@@ -280,127 +268,6 @@ describe('computing a range of oids for a whole object set', () => {
|
|
|
280
268
|
expect(isWithin('test/a1', start, end)).toBe(false);
|
|
281
269
|
expect(isWithin('test/a1:3', start, end)).toBe(false);
|
|
282
270
|
});
|
|
283
|
-
it('should accommodate legacy dot style oids', () => {
|
|
284
|
-
const [start, end] = getLegacyDotOidSubIdRange('test/a.foo:barrrr');
|
|
285
|
-
expect(start).toEqual('test/a.');
|
|
286
|
-
expect(end).toEqual('test/a.\uffff');
|
|
287
|
-
expect(start < end).toBe(true);
|
|
288
|
-
expect(isWithin('test/a.foo:0', start, end)).toBe(true);
|
|
289
|
-
expect(isWithin('test/a.foo:1', start, end)).toBe(true);
|
|
290
|
-
expect(isWithin('test/a.bar:zzzzzzzzzzzzzzzzzzzzzzz', start, end)).toBe(
|
|
291
|
-
true,
|
|
292
|
-
);
|
|
293
|
-
expect(isWithin('test/a.aff:\uffff', start, end)).toBe(true);
|
|
294
|
-
expect(isWithin('test1/a', start, end)).toBe(false);
|
|
295
|
-
expect(isWithin('test/b', start, end)).toBe(false);
|
|
296
|
-
expect(isWithin('test/ ', start, end)).toBe(false);
|
|
297
|
-
expect(isWithin('test/a1', start, end)).toBe(false);
|
|
298
|
-
expect(isWithin('test/a1:3', start, end)).toBe(false);
|
|
299
|
-
expect(isWithin('test/a.foo:barrrr', start, end)).toBe(true);
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
describe('assigning OIDs as properties', () => {
|
|
304
|
-
it('should assign to all sub-objects', () => {
|
|
305
|
-
let i = 0;
|
|
306
|
-
function createSubId() {
|
|
307
|
-
return (i++).toString();
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const initial = {
|
|
311
|
-
foo: {
|
|
312
|
-
bar: 1,
|
|
313
|
-
baz: [2, 3],
|
|
314
|
-
},
|
|
315
|
-
qux: [
|
|
316
|
-
{
|
|
317
|
-
corge: true,
|
|
318
|
-
},
|
|
319
|
-
{
|
|
320
|
-
grault: {
|
|
321
|
-
garply: 4,
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
],
|
|
325
|
-
};
|
|
326
|
-
assignOid(initial, 'test/a');
|
|
327
|
-
assignOidsToAllSubObjects(initial, createSubId);
|
|
328
|
-
assignOidPropertiesToAllSubObjects(initial);
|
|
329
|
-
|
|
330
|
-
expect(initial).toMatchInlineSnapshot(`
|
|
331
|
-
{
|
|
332
|
-
"@@id": "test/a",
|
|
333
|
-
"foo": {
|
|
334
|
-
"@@id": "test/a:0",
|
|
335
|
-
"bar": 1,
|
|
336
|
-
"baz": [
|
|
337
|
-
2,
|
|
338
|
-
3,
|
|
339
|
-
],
|
|
340
|
-
},
|
|
341
|
-
"qux": [
|
|
342
|
-
{
|
|
343
|
-
"@@id": "test/a:3",
|
|
344
|
-
"corge": true,
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
"@@id": "test/a:4",
|
|
348
|
-
"grault": {
|
|
349
|
-
"@@id": "test/a:5",
|
|
350
|
-
"garply": 4,
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
],
|
|
354
|
-
}
|
|
355
|
-
`);
|
|
356
|
-
// extra check needed for array since it doesn't serialize in the snapshot
|
|
357
|
-
expect(maybeGetOidProperty(initial.qux)).toBe('test/a:2');
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('should transfer assigned OID properties to the memory system', () => {
|
|
361
|
-
const initial = assignOidProperty(
|
|
362
|
-
{
|
|
363
|
-
foo: assignOidProperty(
|
|
364
|
-
{
|
|
365
|
-
bar: 1,
|
|
366
|
-
},
|
|
367
|
-
'test/a:1',
|
|
368
|
-
),
|
|
369
|
-
qux: assignOidProperty(
|
|
370
|
-
[
|
|
371
|
-
assignOidProperty(
|
|
372
|
-
{
|
|
373
|
-
corge: true,
|
|
374
|
-
},
|
|
375
|
-
'test/a:2',
|
|
376
|
-
),
|
|
377
|
-
assignOidProperty(
|
|
378
|
-
{
|
|
379
|
-
grault: assignOidProperty(
|
|
380
|
-
{
|
|
381
|
-
garply: 4,
|
|
382
|
-
},
|
|
383
|
-
'test/a:3',
|
|
384
|
-
),
|
|
385
|
-
},
|
|
386
|
-
'test/a:4',
|
|
387
|
-
),
|
|
388
|
-
],
|
|
389
|
-
'test/a:2',
|
|
390
|
-
),
|
|
391
|
-
},
|
|
392
|
-
'test/a',
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
removeOidPropertiesFromAllSubObjects(initial);
|
|
396
|
-
|
|
397
|
-
expect(getOid(initial)).toEqual('test/a');
|
|
398
|
-
expect(getOid(initial.foo)).toEqual('test/a:1');
|
|
399
|
-
expect(getOid(initial.qux)).toEqual('test/a:2');
|
|
400
|
-
expect(getOid(initial.qux[0])).toEqual('test/a:2');
|
|
401
|
-
expect(getOid(initial.qux[1])).toEqual('test/a:4');
|
|
402
|
-
expect(getOid(initial.qux[1].grault)).toEqual('test/a:3');
|
|
403
|
-
});
|
|
404
271
|
});
|
|
405
272
|
|
|
406
273
|
it('should handle special characters in document id or collection', () => {
|
|
@@ -430,111 +297,3 @@ describe('assigning OIDs to sub-objects', () => {
|
|
|
430
297
|
expect(hasOid(obj.bar[0])).toBe(false);
|
|
431
298
|
});
|
|
432
299
|
});
|
|
433
|
-
|
|
434
|
-
describe('handling legacy OIDs', () => {
|
|
435
|
-
it('should get the root OID for a legacy OID', () => {
|
|
436
|
-
expect(getOidRoot('items/clabgyjfh00003968qycsq3ld.inputs.#')).toEqual(
|
|
437
|
-
'items/clabgyjfh00003968qycsq3ld',
|
|
438
|
-
);
|
|
439
|
-
});
|
|
440
|
-
it('should create sub-ids for legacy OIDs in new format', () => {
|
|
441
|
-
expect(
|
|
442
|
-
createSubOid(
|
|
443
|
-
'items/clabgyjfh00003968qycsq3ld.inputs.#',
|
|
444
|
-
() => 'pseudorandom',
|
|
445
|
-
),
|
|
446
|
-
).toEqual('items/clabgyjfh00003968qycsq3ld:pseudorandom');
|
|
447
|
-
});
|
|
448
|
-
it('should identify new sub-OIDs as related to the legacy root OID', () => {
|
|
449
|
-
expect(
|
|
450
|
-
areOidsRelated(
|
|
451
|
-
'items/clabgyjfh00003968qycsq3ld.inputs.#',
|
|
452
|
-
'items/clabgyjfh00003968qycsq3ld:pseudorandom',
|
|
453
|
-
),
|
|
454
|
-
).toBe(true);
|
|
455
|
-
});
|
|
456
|
-
it.each([
|
|
457
|
-
['items/clabgyjfh00003968qycsq3ld.inputs.#:baz', true],
|
|
458
|
-
// include more unicode chars
|
|
459
|
-
['items/clabgyjfh00003968qycsq3ld\ufea3.inputs\u39fc.#:baz!!!', true],
|
|
460
|
-
// not matching new oids
|
|
461
|
-
['items/clabgyjfh00003968qycsq3ld', false],
|
|
462
|
-
['items/clabgyjfh00003968qycsq3ld:baz', false],
|
|
463
|
-
['items/clabgyjfh00003968qycsq3ld:baz1111', false],
|
|
464
|
-
// not matching anything else
|
|
465
|
-
[
|
|
466
|
-
'PREPARE SOUS VIDE BATH: Fill container or pot with water. Set the temperature to 130F/54.4C – 132F/55.5C (for very moist and tender) and allow water to heat to that temperature.Tip: start with hot tap water instead of cold water to reduce heating time. Note 4 for other temperatures.',
|
|
467
|
-
false,
|
|
468
|
-
],
|
|
469
|
-
])('matches legacy oids', (oid, match) => {
|
|
470
|
-
expect(MATCH_LEGACY_OID_JSON_STRING.test('"' + oid + '"'), oid).toBe(match);
|
|
471
|
-
// regex are stateful 🙄
|
|
472
|
-
MATCH_LEGACY_OID_JSON_STRING.lastIndex = 0;
|
|
473
|
-
});
|
|
474
|
-
it.each([
|
|
475
|
-
[
|
|
476
|
-
{ op: 'delete', oid: 'items/clabgyjfh00003968qycsq3ld.inputs.#:baz' },
|
|
477
|
-
{ op: 'delete', oid: 'items/clabgyjfh00003968qycsq3ld:baz' },
|
|
478
|
-
],
|
|
479
|
-
[
|
|
480
|
-
{
|
|
481
|
-
op: 'list-push',
|
|
482
|
-
oid: 'items/clabgyjfh00003968qycsq3ld.inputs.#:baz',
|
|
483
|
-
value: {
|
|
484
|
-
'@@type': 'ref',
|
|
485
|
-
id: 'items/clabgyjfh00003968qycsq3ld.inputs.#:qux',
|
|
486
|
-
},
|
|
487
|
-
},
|
|
488
|
-
{
|
|
489
|
-
op: 'list-push',
|
|
490
|
-
oid: 'items/clabgyjfh00003968qycsq3ld:baz',
|
|
491
|
-
value: { '@@type': 'ref', id: 'items/clabgyjfh00003968qycsq3ld:qux' },
|
|
492
|
-
},
|
|
493
|
-
],
|
|
494
|
-
[
|
|
495
|
-
{
|
|
496
|
-
op: 'list-remove',
|
|
497
|
-
oid: 'items/clabgyjfh00003968qycsq3ld.inputs.#:baz',
|
|
498
|
-
value: {
|
|
499
|
-
'@@type': 'ref',
|
|
500
|
-
id: 'items/clabgyjfh00003968qycsq3ld.inputs.#:qux',
|
|
501
|
-
},
|
|
502
|
-
},
|
|
503
|
-
{
|
|
504
|
-
op: 'list-remove',
|
|
505
|
-
oid: 'items/clabgyjfh00003968qycsq3ld:baz',
|
|
506
|
-
value: { '@@type': 'ref', id: 'items/clabgyjfh00003968qycsq3ld:qux' },
|
|
507
|
-
},
|
|
508
|
-
],
|
|
509
|
-
[
|
|
510
|
-
{
|
|
511
|
-
oid: 'items/clabgyjfh00003968qycsq3ld.inputs.#:baz',
|
|
512
|
-
timestamp: '2021-03-04T21:00:00.000Z',
|
|
513
|
-
snapshot: {
|
|
514
|
-
foo: 1,
|
|
515
|
-
bar: {
|
|
516
|
-
'@@type': 'ref',
|
|
517
|
-
id: 'items/clabgyjfh00003968qycsq3ld.inputs.#:qux',
|
|
518
|
-
},
|
|
519
|
-
},
|
|
520
|
-
},
|
|
521
|
-
{
|
|
522
|
-
oid: 'items/clabgyjfh00003968qycsq3ld:baz',
|
|
523
|
-
timestamp: '2021-03-04T21:00:00.000Z',
|
|
524
|
-
snapshot: {
|
|
525
|
-
foo: 1,
|
|
526
|
-
bar: { '@@type': 'ref', id: 'items/clabgyjfh00003968qycsq3ld:qux' },
|
|
527
|
-
},
|
|
528
|
-
},
|
|
529
|
-
],
|
|
530
|
-
[
|
|
531
|
-
{ oid: 'test/what if.boo.blah so what:fajsdfj' },
|
|
532
|
-
{ oid: 'test/what if:fajsdfj' },
|
|
533
|
-
],
|
|
534
|
-
])(
|
|
535
|
-
'should replace legacy OIDs in a JSON string with new OIDs',
|
|
536
|
-
(from, to) => {
|
|
537
|
-
expect(replaceLegacyOidsInObject(from), JSON.stringify(from)).toEqual(to);
|
|
538
|
-
},
|
|
539
|
-
);
|
|
540
|
-
});
|
package/src/oids.ts
CHANGED
|
@@ -29,10 +29,7 @@ import { isObject, assert } from './utils.js';
|
|
|
29
29
|
|
|
30
30
|
export type ObjectIdentifier = string;
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
export const OID_KEY = '@@id';
|
|
34
|
-
|
|
35
|
-
const COLLECTION_SEPARATOR = '/';
|
|
32
|
+
const SEGMENT_SEPARATOR = '/';
|
|
36
33
|
const RANDOM_SEPARATOR = ':';
|
|
37
34
|
|
|
38
35
|
/**
|
|
@@ -54,7 +51,7 @@ export function maybeGetOid(obj: any): ObjectIdentifier | undefined {
|
|
|
54
51
|
if (!isObject(obj)) {
|
|
55
52
|
return undefined;
|
|
56
53
|
}
|
|
57
|
-
return oidMap.get(obj)
|
|
54
|
+
return oidMap.get(obj);
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
export function assignOid(obj: any, oid: ObjectIdentifier) {
|
|
@@ -78,10 +75,6 @@ export function removeOid(obj: any) {
|
|
|
78
75
|
return obj;
|
|
79
76
|
}
|
|
80
77
|
|
|
81
|
-
export function isOidKey(key: string) {
|
|
82
|
-
return key === OID_KEY || key === LEGACY_OID_KEY;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
78
|
/**
|
|
86
79
|
* For sub-objects, assign a random sub-OID if no OID
|
|
87
80
|
* is already assigned.
|
|
@@ -141,6 +134,11 @@ function unsanitizeFragment(id: string) {
|
|
|
141
134
|
.replace(/˙/g, '.');
|
|
142
135
|
}
|
|
143
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Creates an OID for the document with a particular ID.
|
|
139
|
+
* To create a sub-object OID, use createSubOid and pass
|
|
140
|
+
* the root OID.
|
|
141
|
+
*/
|
|
144
142
|
export function createOid(
|
|
145
143
|
collection: string,
|
|
146
144
|
documentId: string,
|
|
@@ -148,7 +146,7 @@ export function createOid(
|
|
|
148
146
|
) {
|
|
149
147
|
let oid =
|
|
150
148
|
sanitizeFragment(collection) +
|
|
151
|
-
|
|
149
|
+
SEGMENT_SEPARATOR +
|
|
152
150
|
sanitizeFragment(documentId);
|
|
153
151
|
if (subId) {
|
|
154
152
|
oid += RANDOM_SEPARATOR + subId;
|
|
@@ -169,18 +167,26 @@ export function decomposeOid(oid: ObjectIdentifier): {
|
|
|
169
167
|
id: string;
|
|
170
168
|
subId?: string;
|
|
171
169
|
} {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
170
|
+
let [collection, coreId, ...others] = oid.split('/');
|
|
171
|
+
// if 'others' exists, something's off, but maybe we can recover...
|
|
172
|
+
// by assuming the last segment is the authz and bolting the rest onto coreId
|
|
175
173
|
if (others.length) {
|
|
176
|
-
|
|
174
|
+
console.error(
|
|
175
|
+
`OID ${oid} has more than 3 segments. Attempting to parse it anyway.`,
|
|
176
|
+
);
|
|
177
|
+
coreId += '/' + others.join('/');
|
|
177
178
|
}
|
|
179
|
+
|
|
180
|
+
const [idOrLegacyPathId, random] = coreId.split(RANDOM_SEPARATOR);
|
|
181
|
+
|
|
178
182
|
let id;
|
|
183
|
+
// legacy path handling. shouldn't be necessary anymore.
|
|
179
184
|
if (idOrLegacyPathId.includes('.')) {
|
|
180
185
|
id = idOrLegacyPathId.slice(0, idOrLegacyPathId.indexOf('.'));
|
|
181
186
|
} else {
|
|
182
187
|
id = idOrLegacyPathId;
|
|
183
188
|
}
|
|
189
|
+
|
|
184
190
|
return {
|
|
185
191
|
collection: unsanitizeFragment(collection),
|
|
186
192
|
id: unsanitizeFragment(id),
|
|
@@ -230,84 +236,6 @@ export function assignOidsToAllSubObjects(
|
|
|
230
236
|
}
|
|
231
237
|
}
|
|
232
238
|
|
|
233
|
-
export function assignOidProperty(obj: any, oid: ObjectIdentifier) {
|
|
234
|
-
assert(
|
|
235
|
-
isObject(obj),
|
|
236
|
-
`Only objects can be assigned OIDs, received ${JSON.stringify(obj)}`,
|
|
237
|
-
);
|
|
238
|
-
obj[OID_KEY] = oid;
|
|
239
|
-
return obj;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function maybeGetOidProperty(obj: any) {
|
|
243
|
-
if (!isObject(obj)) {
|
|
244
|
-
return undefined;
|
|
245
|
-
}
|
|
246
|
-
return obj[OID_KEY] || obj[LEGACY_OID_KEY];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function removeOidProperty(obj: any) {
|
|
250
|
-
if (!isObject(obj)) {
|
|
251
|
-
return obj;
|
|
252
|
-
}
|
|
253
|
-
delete obj[LEGACY_OID_KEY];
|
|
254
|
-
delete obj[OID_KEY];
|
|
255
|
-
return obj;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function transferOidFromSystemToProperty(obj: any) {
|
|
259
|
-
const oid = maybeGetOid(obj);
|
|
260
|
-
if (oid) {
|
|
261
|
-
assignOidProperty(obj, oid);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Assigns a special property to all objects in the given object
|
|
267
|
-
* which have an OID
|
|
268
|
-
*/
|
|
269
|
-
export function assignOidPropertiesToAllSubObjects(obj: any) {
|
|
270
|
-
transferOidFromSystemToProperty(obj);
|
|
271
|
-
|
|
272
|
-
if (Array.isArray(obj)) {
|
|
273
|
-
for (let i = 0; i < obj.length; i++) {
|
|
274
|
-
assignOidPropertiesToAllSubObjects(obj[i]);
|
|
275
|
-
}
|
|
276
|
-
} else if (isObject(obj)) {
|
|
277
|
-
for (const key of Object.keys(obj)) {
|
|
278
|
-
assignOidPropertiesToAllSubObjects(obj[key]);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function copyOidFromPropertyToSystem(obj: any) {
|
|
284
|
-
const oid = maybeGetOidProperty(obj);
|
|
285
|
-
if (oid) {
|
|
286
|
-
assignOid(obj, oid);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
*
|
|
292
|
-
* Removes the special property from all objects in the given object
|
|
293
|
-
* which have an OID, transferring the OID from the property to the OID
|
|
294
|
-
* system in-memory.
|
|
295
|
-
*/
|
|
296
|
-
export function removeOidPropertiesFromAllSubObjects(obj: any) {
|
|
297
|
-
copyOidFromPropertyToSystem(obj);
|
|
298
|
-
removeOidProperty(obj);
|
|
299
|
-
|
|
300
|
-
if (Array.isArray(obj)) {
|
|
301
|
-
for (let i = 0; i < obj.length; i++) {
|
|
302
|
-
removeOidPropertiesFromAllSubObjects(obj[i]);
|
|
303
|
-
}
|
|
304
|
-
} else if (isObject(obj)) {
|
|
305
|
-
for (const key of Object.keys(obj)) {
|
|
306
|
-
removeOidPropertiesFromAllSubObjects(obj[key]);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
239
|
export function removeOidsFromAllSubObjects(obj: any) {
|
|
312
240
|
removeOid(obj);
|
|
313
241
|
|
|
@@ -442,10 +370,6 @@ export function getOidSubIdRange(oid: ObjectIdentifier) {
|
|
|
442
370
|
const lastSubId = createSubOid(root, () => '\uffff');
|
|
443
371
|
return [`${root}${RANDOM_SEPARATOR}`, lastSubId];
|
|
444
372
|
}
|
|
445
|
-
export function getLegacyDotOidSubIdRange(oid: ObjectIdentifier) {
|
|
446
|
-
const root = getOidRoot(oid);
|
|
447
|
-
return [`${root}.`, `${root}.\uffff`];
|
|
448
|
-
}
|
|
449
373
|
|
|
450
374
|
export function getRoots(oids: ObjectIdentifier[]) {
|
|
451
375
|
const set = new Set<ObjectIdentifier>();
|
|
@@ -459,6 +383,10 @@ export function areOidsRelated(oidA: ObjectIdentifier, oidB: ObjectIdentifier) {
|
|
|
459
383
|
return getOidRoot(oidA) === getOidRoot(oidB);
|
|
460
384
|
}
|
|
461
385
|
|
|
386
|
+
export function isRootOid(oid: ObjectIdentifier) {
|
|
387
|
+
return !oid.includes(RANDOM_SEPARATOR);
|
|
388
|
+
}
|
|
389
|
+
|
|
462
390
|
/**
|
|
463
391
|
* Recursively rewrites any OIDs in an object which are 'foreign' -
|
|
464
392
|
* i.e. relate to some other object/entity - to be local to the
|
|
@@ -493,29 +421,3 @@ function migrateForeignOid(parentOid: ObjectIdentifier, child: any) {
|
|
|
493
421
|
);
|
|
494
422
|
}
|
|
495
423
|
}
|
|
496
|
-
|
|
497
|
-
export function isLegacyDotOid(oid: ObjectIdentifier) {
|
|
498
|
-
const partBeforeRandomSep = oid.split(RANDOM_SEPARATOR)[0];
|
|
499
|
-
return partBeforeRandomSep.includes('.');
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
export function convertLegacyOid(oid: ObjectIdentifier) {
|
|
503
|
-
const { collection, id, subId } = decomposeOid(oid);
|
|
504
|
-
return createOid(collection, id, subId);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
export const MATCH_LEGACY_OID_JSON_STRING = /"\w+\/[^"]+?(\.[^"]+)+\:[\S]+?"/g;
|
|
508
|
-
export function replaceLegacyOidsInJsonString(string: string) {
|
|
509
|
-
// replace every match of a legacy OID, converting to a new OID
|
|
510
|
-
return string.replaceAll(MATCH_LEGACY_OID_JSON_STRING, (match) => {
|
|
511
|
-
const legacyOid = match.slice(1, match.length - 1);
|
|
512
|
-
return `"${convertLegacyOid(legacyOid)}"`;
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
export function replaceLegacyOidsInObject(obj: any) {
|
|
516
|
-
return JSON.parse(replaceLegacyOidsInJsonString(JSON.stringify(obj)));
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
export function isRootOid(oid: ObjectIdentifier) {
|
|
520
|
-
return !oid.includes(RANDOM_SEPARATOR);
|
|
521
|
-
}
|