@verdant-web/common 2.8.0 → 2.9.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.
Files changed (47) hide show
  1. package/dist/esm/authz.js.map +1 -1
  2. package/dist/esm/baseline.d.ts +2 -1
  3. package/dist/esm/diffing.d.ts +3 -2
  4. package/dist/esm/diffing.js +79 -19
  5. package/dist/esm/diffing.js.map +1 -1
  6. package/dist/esm/diffing.test.js +967 -183
  7. package/dist/esm/diffing.test.js.map +1 -1
  8. package/dist/esm/oids.d.ts +5 -0
  9. package/dist/esm/oids.js +23 -1
  10. package/dist/esm/oids.js.map +1 -1
  11. package/dist/esm/operation.d.ts +5 -4
  12. package/dist/esm/operation.js +4 -4
  13. package/dist/esm/operation.js.map +1 -1
  14. package/dist/esm/patch.d.ts +17 -13
  15. package/dist/esm/patch.js +42 -6
  16. package/dist/esm/patch.js.map +1 -1
  17. package/dist/esm/schema/fields.d.ts +1 -0
  18. package/dist/esm/schema/fields.js +5 -0
  19. package/dist/esm/schema/fields.js.map +1 -1
  20. package/dist/esm/schema/validation.d.ts +8 -1
  21. package/dist/esm/schema/validation.js +133 -63
  22. package/dist/esm/schema/validation.js.map +1 -1
  23. package/dist/esm/schema/validation.test.d.ts +1 -0
  24. package/dist/esm/schema/validation.test.js +60 -0
  25. package/dist/esm/schema/validation.test.js.map +1 -0
  26. package/dist/esm/timestamp.d.ts +8 -0
  27. package/dist/esm/timestamp.js +15 -0
  28. package/dist/esm/timestamp.js.map +1 -1
  29. package/dist/esm/undo.d.ts +2 -1
  30. package/dist/esm/undo.js +52 -158
  31. package/dist/esm/undo.js.map +1 -1
  32. package/dist/esm/undo.test.js +68 -7
  33. package/dist/esm/undo.test.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/authz.ts +2 -0
  36. package/src/baseline.ts +3 -1
  37. package/src/diffing.test.ts +1089 -225
  38. package/src/diffing.ts +99 -22
  39. package/src/oids.ts +23 -1
  40. package/src/operation.ts +9 -8
  41. package/src/patch.ts +92 -17
  42. package/src/schema/fields.ts +8 -0
  43. package/src/schema/validation.test.ts +66 -0
  44. package/src/schema/validation.ts +156 -73
  45. package/src/timestamp.ts +14 -0
  46. package/src/undo.test.ts +87 -8
  47. package/src/undo.ts +83 -157
package/src/diffing.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import { AuthorizationKey } from './authz.js';
1
2
  import { VerdantError } from './error.js';
2
3
  import {
3
4
  areOidsRelated,
4
5
  assignOid,
6
+ createRef,
5
7
  getOid,
6
8
  maybeGetOid,
7
9
  ObjectIdentifier,
@@ -27,7 +29,7 @@ export type DiffContext = {
27
29
  /**
28
30
  * Authorization to apply to all created operations
29
31
  */
30
- authz?: string;
32
+ authz?: AuthorizationKey;
31
33
  patchCreator: PatchCreator;
32
34
  };
33
35
 
@@ -70,18 +72,10 @@ function enforceAssignedOid(
70
72
  assignOid(newObject, existingObjectOid);
71
73
  }
72
74
  // NOTE: new OID assignments are done in patchCreator.
73
- // else {
74
- // assignOid(
75
- // newObject,
76
- // createSubOid(parentOid, ctx.patchCreator.createSubId),
77
- // );
78
- // }
79
75
  } else if (!areOidsRelated(parentOid, oid)) {
80
76
  // when there's any doubt, clone the whole object. false -> do not copy OIDs
81
77
  const clone = cloneDeep(newObject, false);
82
78
  // NOTE: new OID assignments are done in patchCreator.
83
- // const cloneOid = createSubOid(parentOid, ctx.patchCreator.createSubId);
84
- // assignOid(clone, cloneOid);
85
79
  return clone;
86
80
  }
87
81
  return newObject;
@@ -102,7 +96,7 @@ export function diffToPatches(
102
96
  merge?: boolean;
103
97
  /** @deprecated - use 'merge' */
104
98
  defaultUndefined?: boolean;
105
- authz?: string;
99
+ authz?: AuthorizationKey;
106
100
  },
107
101
  ) {
108
102
  const ctx: DiffContext = {
@@ -141,7 +135,17 @@ export function diffLists(from: any[], to: any[], ctx: DiffContext) {
141
135
  // from object must be registered with an OID.
142
136
  const oid = getOid(from);
143
137
  // this copy will be mutated to align with inserts, making things easier to compare.
144
- const fromCopy = [...from];
138
+ const fromCopy = [];
139
+ // while iterating from, also collect some metadata.
140
+ const oidsInFrom = new Set();
141
+ for (let i = 0; i < from.length; i++) {
142
+ const value = from[i];
143
+ fromCopy[i] = value;
144
+ const oid = maybeGetOid(value);
145
+ if (oid) {
146
+ oidsInFrom.add(oid);
147
+ }
148
+ }
145
149
 
146
150
  // first, normalize incoming data according to OID rules. this is done early
147
151
  // so future equality checks are according to configuration like mergeUnknownObjects.
@@ -157,12 +161,17 @@ export function diffLists(from: any[], to: any[], ctx: DiffContext) {
157
161
  // track whether the new list has gaps in it. a gap
158
162
  // means we can no longer use list-push for new items.
159
163
  let noGaps = true;
164
+ // track any items we move by reference, so we remember to
165
+ // not delete them later.
166
+ const movedItems = new Set<ObjectIdentifier>();
167
+ const overwrittenItems = new Set<ObjectIdentifier>();
160
168
  for (let i = 0; i < to.length; i++) {
161
169
  const value = to[i];
162
170
  const oldValue = fromCopy[i];
163
171
  if (value === undefined) {
164
172
  noGaps = false;
165
173
  }
174
+ const itemOid = maybeGetOid(value);
166
175
 
167
176
  // we decide if this item is being added to the end of the list if:
168
177
  // - there were no empty spaces before it in the new list
@@ -173,14 +182,49 @@ export function diffLists(from: any[], to: any[], ctx: DiffContext) {
173
182
  // accidentally 'push' into a gap, which would put the item in the wrong place.
174
183
  const isEndOfList = noGaps && i >= fromCopy.length;
175
184
 
176
- if (isEndOfList) {
185
+ // this branch exclusively deals with object references.
186
+ if (itemOid && oidsInFrom.has(itemOid)) {
187
+ // this item was in the old list, so no matter what we want to
188
+ // preserve its identity. the rest of the branches here all
189
+ // assume the item is new and initialize it.
190
+ if (areTheSameIdentity(value, oldValue)) {
191
+ // the item hasn't moved. we just diff it in place.
192
+ diff(oldValue, value, ctx);
193
+ } else {
194
+ // the item has moved. we are currently visiting its new index,
195
+ // so we can move it by reference now
196
+ ctx.patches.push(
197
+ ...ctx.patchCreator.createListMoveByRef(
198
+ oid,
199
+ createRef(itemOid),
200
+ i,
201
+ ctx.authz,
202
+ ),
203
+ );
204
+ // the problem is, a move by ref is not the same as what
205
+ // we really observed here, since it's more like an insertion
206
+ // and not an overwrite. to complicate this, the replaced item
207
+ // at this index might actually have moved, not been replaced.
208
+ // we mark the item that was at this index previously, and if
209
+ // we don't see it later, we'll delete it.
210
+ const fromOid = maybeGetOid(oldValue);
211
+ if (fromOid) {
212
+ overwrittenItems.add(fromOid);
213
+ }
214
+
215
+ // remember this item so we don't delete it later.
216
+ movedItems.add(itemOid);
217
+ }
218
+ } else if (isEndOfList) {
177
219
  // this will initialize all sub-objects in the item, too
178
220
  ctx.patches.push(
179
221
  ...ctx.patchCreator.createListPush(oid, value, ctx.authz),
180
222
  );
181
223
  } else if (areTheSameIdentity(value, oldValue)) {
182
- // the identity of this item hasn't changed, but we
183
- // still have to diff the contents.
224
+ // note : since the branch above covers object references which
225
+ // were present in the original list, logically this branch only
226
+ // covers primitives. either way, call diff, it's just more
227
+ // proper to do so.
184
228
  diff(oldValue, value, ctx);
185
229
  } else {
186
230
  // the identity of this item has changed. we can now evaluate
@@ -220,22 +264,55 @@ export function diffLists(from: any[], to: any[], ctx: DiffContext) {
220
264
  }
221
265
  }
222
266
 
223
- // remove any remaining items at the end of the array
267
+ // remove any remaining items at the end of the array.
224
268
  const deletedItemsAtEnd = fromCopy.length - to.length;
225
269
  if (deletedItemsAtEnd > 0) {
270
+ for (let i = fromCopy.length - 1; i >= to.length; i--) {
271
+ const value = fromCopy[i];
272
+ const itemOid = maybeGetOid(value);
273
+ if (itemOid) {
274
+ // avoid deleting moved objects that were at the end of the list before.
275
+ // their reference was moved elsewhere, but their contents must be retained.
276
+ if (!movedItems.has(itemOid)) {
277
+ // if sub-items were objects, we need to delete them all
278
+ // this should recursively delete children of these items
279
+ // also!
280
+ deleteWithSubObjects(value, ctx);
281
+ ctx.patches.push(
282
+ ...ctx.patchCreator.createListRemove(
283
+ oid,
284
+ createRef(itemOid),
285
+ 'last',
286
+ ctx.authz,
287
+ ),
288
+ );
289
+ }
290
+ } else {
291
+ ctx.patches.push(
292
+ ...ctx.patchCreator.createListRemove(oid, value, 'last', ctx.authz),
293
+ );
294
+ }
295
+ }
296
+ }
297
+
298
+ for (const overwrittenOid of overwrittenItems) {
299
+ if (movedItems.has(overwrittenOid)) {
300
+ // this item was moved, not overwritten. we don't want to delete it.
301
+ continue;
302
+ }
303
+ // this item was overwritten, not moved. we need to delete it.
226
304
  // if sub-items were objects, we need to delete them all
227
305
  // this should recursively delete children of these items
228
306
  // also!
229
- for (let i = to.length; i < fromCopy.length; i++) {
230
- const value = fromCopy[i];
231
- deleteWithSubObjects(value, ctx);
307
+ const item = from.find((x) => maybeGetOid(x) === overwrittenOid);
308
+ if (item) {
309
+ deleteWithSubObjects(item, ctx);
232
310
  }
233
- // push the list-delete for the deleted items
234
311
  ctx.patches.push(
235
- ...ctx.patchCreator.createListDelete(
312
+ ...ctx.patchCreator.createListRemove(
236
313
  oid,
237
- to.length,
238
- deletedItemsAtEnd,
314
+ createRef(overwrittenOid),
315
+ 'last',
239
316
  ctx.authz,
240
317
  ),
241
318
  );
package/src/oids.ts CHANGED
@@ -2,7 +2,7 @@ import { v4 } from 'uuid';
2
2
  import { isFileRef } from './files.js';
3
3
  import { isObjectRef, ObjectRef } from './operation.js';
4
4
  import { isRef } from './refs.js';
5
- import { isObject, assert } from './utils.js';
5
+ import { assert, isObject } from './utils.js';
6
6
 
7
7
  /**
8
8
  * OIDs
@@ -421,3 +421,25 @@ function migrateForeignOid(parentOid: ObjectIdentifier, child: any) {
421
421
  );
422
422
  }
423
423
  }
424
+
425
+ /**
426
+ * Returns a list of all OIDs assigned to this object
427
+ * and all sub-objects.
428
+ */
429
+ export function getAllOids(root: any) {
430
+ const oids = new Set<ObjectIdentifier>();
431
+ const stack = [root];
432
+ while (stack.length) {
433
+ const obj = stack.pop();
434
+ const oid = maybeGetOid(obj);
435
+ if (oid) {
436
+ oids.add(oid);
437
+ }
438
+ if (Array.isArray(obj)) {
439
+ stack.push(...obj);
440
+ } else if (isObject(obj)) {
441
+ stack.push(...Object.values(obj));
442
+ }
443
+ }
444
+ return Array.from(oids);
445
+ }
package/src/operation.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { AuthorizationKey } from './authz.js';
1
2
  import { DocumentBaseline } from './baseline.js';
2
3
  import { FileRef, isFileRef } from './files.js';
3
4
  import {
@@ -151,7 +152,7 @@ export type Operation = {
151
152
  oid: ObjectIdentifier;
152
153
  timestamp: string;
153
154
  data: OperationPatch;
154
- authz?: string;
155
+ authz?: AuthorizationKey;
155
156
  };
156
157
 
157
158
  /**
@@ -164,7 +165,7 @@ export function initialToPatches(
164
165
  getNow: () => string,
165
166
  createSubId?: () => string,
166
167
  patches: Operation[] = [],
167
- options?: { authz?: string },
168
+ options?: { authz?: AuthorizationKey },
168
169
  ) {
169
170
  assignOid(initial, rootOid);
170
171
  assignOidsToAllSubObjects(initial, createSubId);
@@ -189,7 +190,7 @@ export function shallowInitialToPatches(
189
190
  rootOid: ObjectIdentifier,
190
191
  getNow: () => string,
191
192
  patches: Operation[] = [],
192
- options?: { authz?: string },
193
+ options?: { authz?: AuthorizationKey },
193
194
  ) {
194
195
  const op: Operation = {
195
196
  oid: rootOid,
@@ -205,7 +206,7 @@ export function shallowInitialToPatches(
205
206
 
206
207
  // saves a bit of network traffic by not attaching authz
207
208
  // key at all if not present
208
- export function addAuthzToOp(op: Operation, authz?: string) {
209
+ export function addAuthzToOp(op: Operation, authz?: AuthorizationKey) {
209
210
  if (authz) {
210
211
  op.authz = authz;
211
212
  }
@@ -342,18 +343,18 @@ export function applyPatch<T extends NormalizedObject>(
342
343
  do {
343
344
  const valueToRemove = patch.value;
344
345
  if (patch.only === 'last') {
345
- if (isObjectRef(valueToRemove)) {
346
+ if (isRef(valueToRemove)) {
346
347
  index = findLastIndex(
347
348
  base,
348
- (item: any) => item.id === valueToRemove.id,
349
+ (item: any) => isRef(item) && compareRefs(item, valueToRemove),
349
350
  );
350
351
  } else {
351
352
  index = base.lastIndexOf(valueToRemove);
352
353
  }
353
354
  } else {
354
- if (isObjectRef(valueToRemove)) {
355
+ if (isRef(valueToRemove)) {
355
356
  index = base.findIndex(
356
- (item: any) => item.id === valueToRemove.id,
357
+ (item: any) => isRef(item) && compareRefs(item, valueToRemove),
357
358
  );
358
359
  } else {
359
360
  index = base.indexOf(valueToRemove);
package/src/patch.ts CHANGED
@@ -11,9 +11,11 @@ import {
11
11
  ObjectRef,
12
12
  Operation,
13
13
  PropertyName,
14
+ PropertyValue,
15
+ shallowInitialToPatches,
14
16
  } from './operation.js';
15
17
  import { isRef } from './refs.js';
16
- import { isObject } from './utils.js';
18
+ import { assert, isObject } from './utils.js';
17
19
 
18
20
  export class PatchCreator {
19
21
  constructor(
@@ -28,7 +30,13 @@ export class PatchCreator {
28
30
  createDiff = (
29
31
  from: any,
30
32
  to: any,
31
- options: { mergeUnknownObjects?: boolean; defaultUndefined?: boolean } = {},
33
+ options: {
34
+ mergeUnknownObjects?: boolean;
35
+ /** @deprecated use the equivalent 'merge' */
36
+ defaultUndefined?: boolean;
37
+ merge?: boolean;
38
+ authz?: AuthorizationKey;
39
+ } = {},
32
40
  ) => {
33
41
  return diffToPatches(from, to, this.getNow, this.createSubId, [], options);
34
42
  };
@@ -37,7 +45,17 @@ export class PatchCreator {
37
45
  obj: any,
38
46
  oid: ObjectIdentifier,
39
47
  authz?: AuthorizationKey,
48
+ shallow?: boolean,
40
49
  ) => {
50
+ if (shallow) {
51
+ return shallowInitialToPatches(
52
+ obj,
53
+ oid,
54
+ this.getNow,
55
+ undefined,
56
+ authz ? { authz } : undefined,
57
+ );
58
+ }
41
59
  return initialToPatches(
42
60
  obj,
43
61
  oid,
@@ -52,7 +70,7 @@ export class PatchCreator {
52
70
  oid: ObjectIdentifier,
53
71
  key: PropertyName,
54
72
  value: any,
55
- authz?: string,
73
+ authz?: AuthorizationKey,
56
74
  ): Operation[] => {
57
75
  // incoming value must be normalized. if it's not a primitive, it and all sub-objects
58
76
  // must be created
@@ -102,7 +120,7 @@ export class PatchCreator {
102
120
  createRemove = (
103
121
  oid: ObjectIdentifier,
104
122
  key: PropertyName,
105
- authz?: string,
123
+ authz?: AuthorizationKey,
106
124
  ): Operation[] => {
107
125
  return [
108
126
  {
@@ -121,8 +139,9 @@ export class PatchCreator {
121
139
  oid: ObjectIdentifier,
122
140
  index: number,
123
141
  value: any,
124
- authz?: string,
142
+ authz?: AuthorizationKey,
125
143
  ): Operation[] => {
144
+ assert(index >= 0, 'List index must be non-negative');
126
145
  if (this.isPrimitive(value)) {
127
146
  return [
128
147
  {
@@ -166,7 +185,7 @@ export class PatchCreator {
166
185
  createListPush = (
167
186
  oid: ObjectIdentifier,
168
187
  value: any,
169
- authz?: string,
188
+ authz?: AuthorizationKey,
170
189
  ): Operation[] => {
171
190
  if (this.isPrimitive(value)) {
172
191
  return [
@@ -202,7 +221,7 @@ export class PatchCreator {
202
221
  createListAdd = (
203
222
  oid: ObjectIdentifier,
204
223
  value: any,
205
- authz?: string,
224
+ authz?: AuthorizationKey,
206
225
  ): Operation[] => {
207
226
  if (!this.isPrimitive(value)) {
208
227
  return [
@@ -235,8 +254,9 @@ export class PatchCreator {
235
254
  oid: ObjectIdentifier,
236
255
  index: number,
237
256
  value: any,
238
- authz?: string,
257
+ authz?: AuthorizationKey,
239
258
  ): Operation[] => {
259
+ assert(index >= 0, 'List index must be non-negative');
240
260
  if (this.isPrimitive(value)) {
241
261
  return [
242
262
  {
@@ -253,9 +273,18 @@ export class PatchCreator {
253
273
  } else {
254
274
  const itemOid = createSubOid(oid, this.createSubId);
255
275
  return [
256
- ...initialToPatches(value, itemOid, this.getNow, undefined, undefined, {
257
- authz,
258
- }),
276
+ ...initialToPatches(
277
+ value,
278
+ itemOid,
279
+ this.getNow,
280
+ this.createSubId,
281
+ undefined,
282
+ authz
283
+ ? {
284
+ authz,
285
+ }
286
+ : undefined,
287
+ ),
259
288
  {
260
289
  oid,
261
290
  timestamp: this.getNow(),
@@ -270,11 +299,49 @@ export class PatchCreator {
270
299
  }
271
300
  };
272
301
 
302
+ createListInsertMany = (
303
+ oid: ObjectIdentifier,
304
+ index: number,
305
+ values: any[],
306
+ authz?: AuthorizationKey,
307
+ ): Operation[] => {
308
+ assert(index >= 0, 'List index must be non-negative');
309
+ const operations: Operation[] = [];
310
+ const refs = values.map((value) => {
311
+ if (this.isPrimitive(value)) return value;
312
+ const subOid = createSubOid(oid, this.createSubId);
313
+ operations.push(
314
+ ...initialToPatches(
315
+ value,
316
+ subOid,
317
+ this.getNow,
318
+ this.createSubId,
319
+ undefined,
320
+ {
321
+ authz,
322
+ },
323
+ ),
324
+ );
325
+ return createRef(subOid);
326
+ });
327
+ operations.push({
328
+ oid,
329
+ timestamp: this.getNow(),
330
+ data: {
331
+ op: 'list-insert',
332
+ values: refs,
333
+ index,
334
+ },
335
+ authz,
336
+ });
337
+ return operations;
338
+ };
339
+
273
340
  createListRemove = (
274
341
  oid: ObjectIdentifier,
275
- value: any,
342
+ value: PropertyValue,
276
343
  only?: 'first' | 'last',
277
- authz?: string,
344
+ authz?: AuthorizationKey,
278
345
  ): Operation[] => {
279
346
  return [
280
347
  {
@@ -294,8 +361,10 @@ export class PatchCreator {
294
361
  oid: ObjectIdentifier,
295
362
  index: number,
296
363
  count: number = 1,
297
- authz?: string,
364
+ authz?: AuthorizationKey,
298
365
  ): Operation[] => {
366
+ assert(index >= 0, 'List index must be non-negative');
367
+ assert(count > 0, 'Count must be positive and non-zero');
299
368
  return [
300
369
  {
301
370
  oid,
@@ -314,8 +383,9 @@ export class PatchCreator {
314
383
  oid: ObjectIdentifier,
315
384
  value: ObjectRef | FileRef,
316
385
  index: number,
317
- authz?: string,
386
+ authz?: AuthorizationKey,
318
387
  ): Operation[] => {
388
+ assert(index >= 0, 'List index must be non-negative');
319
389
  return [
320
390
  {
321
391
  oid,
@@ -334,8 +404,10 @@ export class PatchCreator {
334
404
  oid: ObjectIdentifier,
335
405
  fromIndex: number,
336
406
  toIndex: number,
337
- authz?: string,
407
+ authz?: AuthorizationKey,
338
408
  ): Operation[] => {
409
+ assert(fromIndex >= 0, 'List move from index must be non-negative');
410
+ assert(toIndex >= 0, 'List move to index must be non-negative');
339
411
  return [
340
412
  {
341
413
  oid,
@@ -350,7 +422,10 @@ export class PatchCreator {
350
422
  ];
351
423
  };
352
424
 
353
- createDelete = (oid: ObjectIdentifier, authz?: string): Operation[] => {
425
+ createDelete = (
426
+ oid: ObjectIdentifier,
427
+ authz?: AuthorizationKey,
428
+ ): Operation[] => {
354
429
  return [
355
430
  {
356
431
  oid,
@@ -156,3 +156,11 @@ export function traverseCollectionFieldsAndRemoveExtraProperties(
156
156
  }
157
157
  }
158
158
  }
159
+
160
+ export function isPrimitive(fieldSchema: StorageFieldSchema) {
161
+ return (
162
+ fieldSchema.type === 'string' ||
163
+ fieldSchema.type === 'number' ||
164
+ fieldSchema.type === 'boolean'
165
+ );
166
+ }
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createFileRef } from '../files.js';
3
+ import { createRef } from '../oids.js';
4
+ import { schema, validateEntityField } from './index.js';
5
+
6
+ describe('validation', () => {
7
+ it('validates shallowly with refs', () => {
8
+ const fieldSchema = schema.fields.object({
9
+ fields: {
10
+ obj: schema.fields.object({
11
+ fields: {},
12
+ }),
13
+ file: schema.fields.file(),
14
+ },
15
+ });
16
+ expect(
17
+ validateEntityField({
18
+ field: fieldSchema,
19
+ value: {
20
+ obj: createRef('some/id:1'),
21
+ file: createFileRef('somefile'),
22
+ },
23
+ fieldPath: [],
24
+ expectRefs: true,
25
+ }),
26
+ ).toBeUndefined();
27
+ expect(
28
+ validateEntityField({
29
+ field: fieldSchema,
30
+ value: {
31
+ obj: createRef('some/id:1'),
32
+ file: createRef('some/object:1'),
33
+ },
34
+ fieldPath: [],
35
+ expectRefs: true,
36
+ }),
37
+ ).toMatchInlineSnapshot(`
38
+ {
39
+ "fieldPath": [
40
+ "file",
41
+ ],
42
+ "message": "Expected file ref for field file, got {"@@type":"ref","id":"some/object:1"}",
43
+ "type": "invalid-type",
44
+ }
45
+ `);
46
+ expect(
47
+ validateEntityField({
48
+ field: fieldSchema,
49
+ value: {
50
+ obj: 'foo',
51
+ file: createFileRef('somefile'),
52
+ },
53
+ fieldPath: [],
54
+ expectRefs: true,
55
+ }),
56
+ ).toMatchInlineSnapshot(`
57
+ {
58
+ "fieldPath": [
59
+ "obj",
60
+ ],
61
+ "message": "Expected ref for field obj, got "foo"",
62
+ "type": "invalid-type",
63
+ }
64
+ `);
65
+ });
66
+ });