@verdant-web/common 2.3.3 → 2.4.0-next.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.
Files changed (60) hide show
  1. package/LICENSE +650 -21
  2. package/dist/esm/authz.d.ts +25 -0
  3. package/dist/esm/authz.js +54 -0
  4. package/dist/esm/authz.js.map +1 -0
  5. package/dist/esm/baseline.d.ts +1 -0
  6. package/dist/esm/baseline.js +3 -0
  7. package/dist/esm/baseline.js.map +1 -1
  8. package/dist/esm/files.d.ts +4 -0
  9. package/dist/esm/files.js +18 -0
  10. package/dist/esm/files.js.map +1 -1
  11. package/dist/esm/index.d.ts +2 -0
  12. package/dist/esm/index.js +2 -0
  13. package/dist/esm/index.js.map +1 -1
  14. package/dist/esm/indexes.js +1 -1
  15. package/dist/esm/indexes.js.map +1 -1
  16. package/dist/esm/indexes.test.js +1 -1
  17. package/dist/esm/indexes.test.js.map +1 -1
  18. package/dist/esm/migration.d.ts +4 -2
  19. package/dist/esm/migration.js.map +1 -1
  20. package/dist/esm/oids.d.ts +6 -24
  21. package/dist/esm/oids.js +18 -107
  22. package/dist/esm/oids.js.map +1 -1
  23. package/dist/esm/oids.test.js +1 -188
  24. package/dist/esm/oids.test.js.map +1 -1
  25. package/dist/esm/oidsLegacy.d.ts +18 -0
  26. package/dist/esm/oidsLegacy.js +72 -0
  27. package/dist/esm/oidsLegacy.js.map +1 -0
  28. package/dist/esm/oidsLegacy.test.d.ts +1 -0
  29. package/dist/esm/oidsLegacy.test.js +115 -0
  30. package/dist/esm/oidsLegacy.test.js.map +1 -0
  31. package/dist/esm/operation.d.ts +28 -4
  32. package/dist/esm/operation.js +70 -35
  33. package/dist/esm/operation.js.map +1 -1
  34. package/dist/esm/operation.test.js.map +1 -1
  35. package/dist/esm/presence.d.ts +0 -2
  36. package/dist/esm/schema/fields.js +1 -1
  37. package/dist/esm/schema/fields.js.map +1 -1
  38. package/dist/esm/schema/validation.js +14 -7
  39. package/dist/esm/schema/validation.js.map +1 -1
  40. package/dist/esm/utils.d.ts +1 -0
  41. package/dist/esm/utils.js +21 -0
  42. package/dist/esm/utils.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/authz.ts +67 -0
  45. package/src/baseline.ts +5 -4
  46. package/src/files.ts +25 -0
  47. package/src/index.ts +2 -0
  48. package/src/indexes.test.ts +1 -1
  49. package/src/indexes.ts +1 -1
  50. package/src/migration.ts +5 -1
  51. package/src/oids.test.ts +0 -241
  52. package/src/oids.ts +24 -122
  53. package/src/oidsLegacy.test.ts +147 -0
  54. package/src/oidsLegacy.ts +87 -0
  55. package/src/operation.test.ts +0 -1
  56. package/src/operation.ts +202 -103
  57. package/src/presence.ts +0 -2
  58. package/src/schema/fields.ts +1 -1
  59. package/src/schema/validation.ts +23 -14
  60. package/src/utils.ts +26 -0
package/src/operation.ts CHANGED
@@ -9,12 +9,12 @@ import {
9
9
  ensureOid,
10
10
  getOid,
11
11
  getOidRoot,
12
- isOidKey,
13
12
  maybeGetOid,
14
13
  normalize,
15
14
  ObjectIdentifier,
16
15
  removeOid,
17
16
  } from './oids.js';
17
+ import { isOidKey } from './oidsLegacy.js';
18
18
  import { compareRefs, isRef } from './refs.js';
19
19
  import { assert, cloneDeep, findLastIndex, isObject } from './utils.js';
20
20
 
@@ -156,6 +156,7 @@ export type Operation = {
156
156
  oid: ObjectIdentifier;
157
157
  timestamp: string;
158
158
  data: OperationPatch;
159
+ authz?: string;
159
160
  };
160
161
 
161
162
  function isDiffableObject(val: any) {
@@ -174,7 +175,7 @@ export function diffToPatches<T extends { [key: string]: any } | any[]>(
174
175
  getNow: () => string,
175
176
  createSubId?: () => string,
176
177
  patches: Operation[] = [],
177
- options: {
178
+ options?: {
178
179
  /**
179
180
  * If an object is merged with another and the new one does not
180
181
  * have an OID assigned, assume it is the same identity as previous
@@ -185,10 +186,16 @@ export function diffToPatches<T extends { [key: string]: any } | any[]>(
185
186
  * If false, undefined properties will erase the previous value.
186
187
  */
187
188
  defaultUndefined?: boolean;
188
- } = {},
189
+ /**
190
+ * Authorization to apply to all created operations
191
+ */
192
+ authz?: string;
193
+ },
189
194
  ): Operation[] {
195
+ const authz = options?.authz;
190
196
  const oid = getOid(from);
191
197
 
198
+ // FIXME: allocating this function inline seems wasteful.
192
199
  function diffItems(
193
200
  key: string | number,
194
201
  value: any,
@@ -200,25 +207,35 @@ export function diffToPatches<T extends { [key: string]: any } | any[]>(
200
207
  // do not need to recurse, of course
201
208
  if (!compareNonDiffable(value, oldValue)) {
202
209
  if (isList) {
203
- patches.push({
204
- oid,
205
- timestamp: getNow(),
206
- data: {
207
- op: 'list-set',
208
- index: key as number,
209
- value,
210
- },
211
- });
210
+ patches.push(
211
+ addAuthzToOp(
212
+ {
213
+ oid,
214
+ timestamp: getNow(),
215
+ data: {
216
+ op: 'list-set',
217
+ index: key as number,
218
+ value,
219
+ },
220
+ },
221
+ authz,
222
+ ),
223
+ );
212
224
  } else {
213
- patches.push({
214
- oid,
215
- timestamp: getNow(),
216
- data: {
217
- op: 'set',
218
- name: key,
219
- value,
220
- },
221
- });
225
+ patches.push(
226
+ addAuthzToOp(
227
+ {
228
+ oid,
229
+ timestamp: getNow(),
230
+ data: {
231
+ op: 'set',
232
+ name: key,
233
+ value,
234
+ },
235
+ },
236
+ authz,
237
+ ),
238
+ );
222
239
  }
223
240
  }
224
241
  } else {
@@ -231,7 +248,7 @@ export function diffToPatches<T extends { [key: string]: any } | any[]>(
231
248
  value = cloneDeep(value, false);
232
249
  valueOid = createSubOid(oid, createSubId);
233
250
  assignOid(value, valueOid);
234
- } else if (!options.mergeUnknownObjects) {
251
+ } else if (!options?.mergeUnknownObjects) {
235
252
  valueOid = ensureOid(value, oid, createSubId);
236
253
  } else {
237
254
  // if merge unknown objects is requested, we copy the previous value's oid
@@ -258,24 +275,34 @@ export function diffToPatches<T extends { [key: string]: any } | any[]>(
258
275
  // we add the whole new object and also update the reference on this
259
276
  // key of the parent to point to the new object
260
277
  initialToPatches(value, valueOid, getNow, createSubId, patches);
261
- patches.push({
262
- oid,
263
- timestamp: getNow(),
264
- data: {
265
- op: 'set',
266
- name: key,
267
- value: createRef(valueOid),
268
- },
269
- });
278
+ patches.push(
279
+ addAuthzToOp(
280
+ {
281
+ oid,
282
+ timestamp: getNow(),
283
+ data: {
284
+ op: 'set',
285
+ name: key,
286
+ value: createRef(valueOid),
287
+ },
288
+ },
289
+ authz,
290
+ ),
291
+ );
270
292
  // if there was an old value, we need to delete it altogether.
271
293
  if (oldValueOid !== undefined) {
272
- patches.push({
273
- oid: oldValueOid,
274
- timestamp: getNow(),
275
- data: {
276
- op: 'delete',
277
- },
278
- });
294
+ patches.push(
295
+ addAuthzToOp(
296
+ {
297
+ oid: oldValueOid,
298
+ timestamp: getNow(),
299
+ data: {
300
+ op: 'delete',
301
+ },
302
+ },
303
+ authz,
304
+ ),
305
+ );
279
306
  }
280
307
  } else {
281
308
  // third case: OIDs are the same, meaning the identity is the same,
@@ -302,32 +329,42 @@ export function diffToPatches<T extends { [key: string]: any } | any[]>(
302
329
  const value = from[i];
303
330
  const valueOid = maybeGetOid(value);
304
331
  if (valueOid) {
305
- patches.push({
306
- oid: valueOid,
307
- timestamp: getNow(),
308
- data: {
309
- op: 'delete',
310
- },
311
- });
332
+ patches.push(
333
+ addAuthzToOp(
334
+ {
335
+ oid: valueOid,
336
+ timestamp: getNow(),
337
+ data: {
338
+ op: 'delete',
339
+ },
340
+ },
341
+ authz,
342
+ ),
343
+ );
312
344
  }
313
345
  }
314
346
  // push the list-delete for the deleted items
315
- patches.push({
316
- oid,
317
- timestamp: getNow(),
318
- data: {
319
- op: 'list-delete',
320
- index: to.length,
321
- count: deletedItemsAtEnd,
322
- },
323
- });
347
+ patches.push(
348
+ addAuthzToOp(
349
+ {
350
+ oid,
351
+ timestamp: getNow(),
352
+ data: {
353
+ op: 'list-delete',
354
+ index: to.length,
355
+ count: deletedItemsAtEnd,
356
+ },
357
+ },
358
+ authz,
359
+ ),
360
+ );
324
361
  }
325
362
  } else if (Array.isArray(from) || Array.isArray(to)) {
326
363
  throw new Error('Cannot diff an array with an object');
327
364
  } else if (isDiffableObject(from) && isDiffableObject(to)) {
328
365
  const oldKeys = new Set(Object.keys(from));
329
366
  for (const [key, value] of Object.entries(to)) {
330
- if (value === undefined && options.defaultUndefined) continue;
367
+ if (value === undefined && options?.defaultUndefined) continue;
331
368
 
332
369
  oldKeys.delete(key);
333
370
 
@@ -339,18 +376,23 @@ export function diffToPatches<T extends { [key: string]: any } | any[]>(
339
376
  }
340
377
 
341
378
  // this set now only contains keys which were not in the new object
342
- if (!options.defaultUndefined) {
379
+ if (!options?.defaultUndefined) {
343
380
  for (const key of oldKeys) {
344
381
  if (isOidKey(key)) continue;
345
382
  // push the delete for the property
346
- patches.push({
347
- oid,
348
- timestamp: getNow(),
349
- data: {
350
- op: 'remove',
351
- name: key,
352
- },
353
- });
383
+ patches.push(
384
+ addAuthzToOp(
385
+ {
386
+ oid,
387
+ timestamp: getNow(),
388
+ data: {
389
+ op: 'remove',
390
+ name: key,
391
+ },
392
+ },
393
+ authz,
394
+ ),
395
+ );
354
396
  }
355
397
  }
356
398
  }
@@ -363,8 +405,10 @@ export function shallowDiffToPatches(
363
405
  to: any,
364
406
  getNow: () => string,
365
407
  patches: Operation[] = [],
408
+ options?: { authz?: string },
366
409
  ) {
367
410
  const oid = getOid(from);
411
+ const authz = options?.authz;
368
412
 
369
413
  function diffItems(
370
414
  key: string | number,
@@ -377,25 +421,35 @@ export function shallowDiffToPatches(
377
421
  // do not need to recurse, of course
378
422
  if (!compareNonDiffable(value, oldValue)) {
379
423
  if (isList) {
380
- patches.push({
381
- oid,
382
- timestamp: getNow(),
383
- data: {
384
- op: 'list-set',
385
- index: key as number,
386
- value,
387
- },
388
- });
424
+ patches.push(
425
+ addAuthzToOp(
426
+ {
427
+ oid,
428
+ timestamp: getNow(),
429
+ data: {
430
+ op: 'list-set',
431
+ index: key as number,
432
+ value,
433
+ },
434
+ },
435
+ authz,
436
+ ),
437
+ );
389
438
  } else {
390
- patches.push({
391
- oid,
392
- timestamp: getNow(),
393
- data: {
394
- op: 'set',
395
- name: key,
396
- value,
397
- },
398
- });
439
+ patches.push(
440
+ addAuthzToOp(
441
+ {
442
+ oid,
443
+ timestamp: getNow(),
444
+ data: {
445
+ op: 'set',
446
+ name: key,
447
+ value,
448
+ },
449
+ },
450
+ authz,
451
+ ),
452
+ );
399
453
  }
400
454
  }
401
455
  } else {
@@ -418,15 +472,20 @@ export function shallowDiffToPatches(
418
472
  const deletedItemsAtEnd = from.length - to.length;
419
473
  if (deletedItemsAtEnd > 0) {
420
474
  // push the list-delete for the deleted items
421
- patches.push({
422
- oid,
423
- timestamp: getNow(),
424
- data: {
425
- op: 'list-delete',
426
- index: to.length,
427
- count: deletedItemsAtEnd,
428
- },
429
- });
475
+ patches.push(
476
+ addAuthzToOp(
477
+ {
478
+ oid,
479
+ timestamp: getNow(),
480
+ data: {
481
+ op: 'list-delete',
482
+ index: to.length,
483
+ count: deletedItemsAtEnd,
484
+ },
485
+ },
486
+ authz,
487
+ ),
488
+ );
430
489
  }
431
490
  } else if (Array.isArray(from) || Array.isArray(to)) {
432
491
  throw new Error('Cannot diff an array with an object');
@@ -446,14 +505,19 @@ export function shallowDiffToPatches(
446
505
  for (const key of oldKeys) {
447
506
  if (isOidKey(key)) continue;
448
507
  // push the delete for the property
449
- patches.push({
450
- oid,
451
- timestamp: getNow(),
452
- data: {
453
- op: 'remove',
454
- name: key,
455
- },
456
- });
508
+ patches.push(
509
+ addAuthzToOp(
510
+ {
511
+ oid,
512
+ timestamp: getNow(),
513
+ data: {
514
+ op: 'remove',
515
+ name: key,
516
+ },
517
+ },
518
+ authz,
519
+ ),
520
+ );
457
521
  }
458
522
  }
459
523
 
@@ -470,20 +534,22 @@ export function initialToPatches(
470
534
  getNow: () => string,
471
535
  createSubId?: () => string,
472
536
  patches: Operation[] = [],
537
+ options?: { authz?: string },
473
538
  ) {
474
539
  assignOid(initial, rootOid);
475
540
  assignOidsToAllSubObjects(initial, createSubId);
476
541
  const normalized = normalize(initial);
477
542
  for (const key of normalized.keys()) {
478
543
  const value = normalized.get(key);
479
- patches.push({
544
+ const op: Operation = {
480
545
  oid: key,
481
546
  timestamp: getNow(),
482
547
  data: {
483
548
  op: 'initialize',
484
549
  value: removeOid(value),
485
550
  },
486
- });
551
+ };
552
+ patches.push(addAuthzToOp(op, options?.authz));
487
553
  }
488
554
  return patches;
489
555
  }
@@ -493,18 +559,29 @@ export function shallowInitialToPatches(
493
559
  rootOid: ObjectIdentifier,
494
560
  getNow: () => string,
495
561
  patches: Operation[] = [],
562
+ options?: { authz?: string },
496
563
  ) {
497
- patches.push({
564
+ const op: Operation = {
498
565
  oid: rootOid,
499
566
  timestamp: getNow(),
500
567
  data: {
501
568
  op: 'initialize',
502
569
  value: initial,
503
570
  },
504
- });
571
+ };
572
+ patches.push(addAuthzToOp(op, options?.authz));
505
573
  return patches;
506
574
  }
507
575
 
576
+ // saves a bit of network traffic by not attaching authz
577
+ // key at all if not present
578
+ function addAuthzToOp(op: Operation, authz?: string) {
579
+ if (authz) {
580
+ op.authz = authz;
581
+ }
582
+ return op;
583
+ }
584
+
508
585
  export function groupPatchesByOid(patches: Operation[]) {
509
586
  const grouped: Record<ObjectIdentifier, Operation[]> = {};
510
587
  for (const patch of patches) {
@@ -719,10 +796,18 @@ export function applyOperations<T extends NormalizedObject>(
719
796
  base: T | undefined,
720
797
  operations: Operation[],
721
798
  deletedRefs?: (ObjectRef | FileRef)[],
799
+ /**
800
+ * Provide and it will be assigned any authz info assigned
801
+ * in applied operations
802
+ */
803
+ authzRef?: { authz?: string },
722
804
  ): T | undefined {
723
805
  let cur = base as T | undefined;
724
806
  for (const op of operations) {
725
807
  cur = applyPatch(cur, op.data, deletedRefs);
808
+ if (authzRef && op.data.op === 'initialize' && op.authz) {
809
+ authzRef.authz = op.authz;
810
+ }
726
811
  }
727
812
  return cur;
728
813
  }
@@ -841,3 +926,17 @@ export function isSuperseded(
841
926
 
842
927
  return false;
843
928
  }
929
+
930
+ /**
931
+ * Allocates a copy with only valid keys for transmitting over
932
+ * the protocol. TODO: make this unnecessary or evaluate if it's
933
+ * worth it to do.
934
+ */
935
+ export function pickValidOperationKeys(operation: Operation): Operation {
936
+ return {
937
+ oid: operation.oid,
938
+ timestamp: operation.timestamp,
939
+ data: operation.data,
940
+ authz: operation.authz,
941
+ };
942
+ }
package/src/presence.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  export interface UserInfo<Profile, Presence> {
2
2
  /**
3
- * @deprecated - include the ID in your Profile data instead.
4
- *
5
3
  * This is the ID representing the user who is utilizing a
6
4
  * replica client to connect to the storage network.
7
5
  * One user may have multiple replica clients active at once,
@@ -1,4 +1,4 @@
1
- import { LEGACY_OID_KEY, OID_KEY } from '../oids.js';
1
+ import { LEGACY_OID_KEY, OID_KEY } from '../oidsLegacy.js';
2
2
  import { isObject } from '../utils.js';
3
3
  import type {
4
4
  StorageFieldSchema,
@@ -1,4 +1,5 @@
1
- import { OID_KEY } from '../oids.js';
1
+ import { isFile, isFileData } from '../files.js';
2
+ import { OID_KEY } from '../oidsLegacy.js';
2
3
  import { isObject } from '../utils.js';
3
4
  import { getFieldDefault, hasDefault, isNullable } from './fields.js';
4
5
  import { StorageFieldSchema, StorageFieldsSchema } from './types.js';
@@ -71,9 +72,9 @@ export function validateEntityField({
71
72
  return {
72
73
  type: 'invalid-type',
73
74
  fieldPath,
74
- message: `Expected object${
75
- field.nullable ? ' or null' : ''
76
- } for field ${formatField(fieldPath)}, got ${value}`,
75
+ message: `Expected object ${
76
+ field.nullable ? 'or null ' : ''
77
+ }for field ${formatField(fieldPath)}, got ${value}`,
77
78
  };
78
79
  }
79
80
  for (const [key, subField] of Object.entries(
@@ -108,9 +109,9 @@ export function validateEntityField({
108
109
  return {
109
110
  type: 'invalid-value',
110
111
  fieldPath,
111
- message: `Expected array${
112
- field.nullable ? ' or null' : ''
113
- } for field ${formatField(fieldPath)}, got ${value}`,
112
+ message: `Expected array ${
113
+ field.nullable ? 'or null ' : ''
114
+ }for field ${formatField(fieldPath)}, got ${value}`,
114
115
  };
115
116
  }
116
117
  for (const item of value) {
@@ -145,8 +146,8 @@ export function validateEntityField({
145
146
  type: 'invalid-type',
146
147
  fieldPath,
147
148
  message: `Expected string ${
148
- field.nullable ? ' or null' : ''
149
- } for field ${formatField(fieldPath)}, got ${value}`,
149
+ field.nullable ? 'or null ' : ''
150
+ }for field ${formatField(fieldPath)}, got ${value}`,
150
151
  };
151
152
  }
152
153
  if (field.options && !field.options.includes(value)) {
@@ -164,8 +165,8 @@ export function validateEntityField({
164
165
  type: 'invalid-type',
165
166
  fieldPath,
166
167
  message: `Expected boolean ${
167
- field.nullable ? ' or null' : ''
168
- } for field ${formatField(fieldPath)}, got ${value}`,
168
+ field.nullable ? 'or null ' : ''
169
+ }for field ${formatField(fieldPath)}, got ${value}`,
169
170
  };
170
171
  }
171
172
  } else if (field.type === 'number') {
@@ -174,12 +175,20 @@ export function validateEntityField({
174
175
  type: 'invalid-type',
175
176
  fieldPath,
176
177
  message: `Expected number ${
177
- field.nullable ? ' or null' : ''
178
- } for field ${formatField(fieldPath)}, got ${value}`,
178
+ field.nullable ? 'or null ' : ''
179
+ }for field ${formatField(fieldPath)}, got ${value}`,
179
180
  };
180
181
  }
181
182
  } else if (field.type === 'file') {
182
- // TODO: how?
183
+ if (!isFile(value) && !isFileData(value)) {
184
+ return {
185
+ type: 'invalid-type',
186
+ fieldPath,
187
+ message: `Expected file ${
188
+ field.nullable ? 'or null ' : ''
189
+ }for field ${formatField(fieldPath)}, got ${value}`,
190
+ };
191
+ }
183
192
  }
184
193
  }
185
194
 
package/src/utils.ts CHANGED
@@ -153,3 +153,29 @@ export function debounce<T extends (...args: any[]) => any>(
153
153
  timeout = setTimeout(() => fn.apply(context, args), wait);
154
154
  } as any;
155
155
  }
156
+
157
+ export function throttle<T extends (...args: any[]) => any>(
158
+ fn: T,
159
+ wait: number,
160
+ ): T {
161
+ let lastTime = 0;
162
+
163
+ // invoke once for the last call
164
+ let trailingTimeout: any;
165
+
166
+ return function (this: any, ...args: any[]) {
167
+ const context = this;
168
+ const now = Date.now();
169
+ if (now - lastTime >= wait) {
170
+ lastTime = now;
171
+ fn.apply(context, args);
172
+ clearTimeout(trailingTimeout);
173
+ } else {
174
+ clearTimeout(trailingTimeout);
175
+ trailingTimeout = setTimeout(() => {
176
+ lastTime = now;
177
+ fn.apply(context, args);
178
+ }, wait);
179
+ }
180
+ } as any;
181
+ }