@verdant-web/common 1.16.2 → 2.0.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 (105) hide show
  1. package/dist/cjs/EventSubscriber.d.ts +1 -0
  2. package/dist/cjs/EventSubscriber.js +3 -0
  3. package/dist/cjs/EventSubscriber.js.map +1 -1
  4. package/dist/cjs/batching.d.ts +1 -0
  5. package/dist/cjs/batching.js +6 -0
  6. package/dist/cjs/batching.js.map +1 -1
  7. package/dist/cjs/index.d.ts +1 -0
  8. package/dist/cjs/index.js +1 -0
  9. package/dist/cjs/index.js.map +1 -1
  10. package/dist/cjs/memo.d.ts +6 -0
  11. package/dist/cjs/memo.js +25 -0
  12. package/dist/cjs/memo.js.map +1 -0
  13. package/dist/cjs/memo.test.d.ts +1 -0
  14. package/dist/cjs/memo.test.js +29 -0
  15. package/dist/cjs/memo.test.js.map +1 -0
  16. package/dist/cjs/migration.js +4 -0
  17. package/dist/cjs/migration.js.map +1 -1
  18. package/dist/cjs/oids.d.ts +1 -0
  19. package/dist/cjs/oids.js +5 -1
  20. package/dist/cjs/oids.js.map +1 -1
  21. package/dist/cjs/operation.d.ts +7 -4
  22. package/dist/cjs/operation.js +7 -4
  23. package/dist/cjs/operation.js.map +1 -1
  24. package/dist/cjs/patch.d.ts +2 -1
  25. package/dist/cjs/patch.js.map +1 -1
  26. package/dist/cjs/refs.d.ts +1 -1
  27. package/dist/cjs/schema/children.d.ts +2 -0
  28. package/dist/cjs/schema/children.js +24 -0
  29. package/dist/cjs/schema/children.js.map +1 -0
  30. package/dist/cjs/schema/fields.d.ts +3 -0
  31. package/dist/cjs/schema/fields.js +33 -2
  32. package/dist/cjs/schema/fields.js.map +1 -1
  33. package/dist/cjs/schema/index.d.ts +1 -0
  34. package/dist/cjs/schema/index.js +1 -0
  35. package/dist/cjs/schema/index.js.map +1 -1
  36. package/dist/cjs/schema/validation.d.ts +14 -3
  37. package/dist/cjs/schema/validation.js +88 -30
  38. package/dist/cjs/schema/validation.js.map +1 -1
  39. package/dist/cjs/timestamp.d.ts +2 -1
  40. package/dist/cjs/timestamp.js +6 -4
  41. package/dist/cjs/timestamp.js.map +1 -1
  42. package/dist/cjs/undo.js +3 -1
  43. package/dist/cjs/undo.js.map +1 -1
  44. package/dist/esm/EventSubscriber.d.ts +1 -0
  45. package/dist/esm/EventSubscriber.js +3 -0
  46. package/dist/esm/EventSubscriber.js.map +1 -1
  47. package/dist/esm/batching.d.ts +1 -0
  48. package/dist/esm/batching.js +6 -0
  49. package/dist/esm/batching.js.map +1 -1
  50. package/dist/esm/index.d.ts +1 -0
  51. package/dist/esm/index.js +1 -0
  52. package/dist/esm/index.js.map +1 -1
  53. package/dist/esm/memo.d.ts +6 -0
  54. package/dist/esm/memo.js +21 -0
  55. package/dist/esm/memo.js.map +1 -0
  56. package/dist/esm/memo.test.d.ts +1 -0
  57. package/dist/esm/memo.test.js +27 -0
  58. package/dist/esm/memo.test.js.map +1 -0
  59. package/dist/esm/migration.js +5 -1
  60. package/dist/esm/migration.js.map +1 -1
  61. package/dist/esm/oids.d.ts +1 -0
  62. package/dist/esm/oids.js +3 -0
  63. package/dist/esm/oids.js.map +1 -1
  64. package/dist/esm/operation.d.ts +7 -4
  65. package/dist/esm/operation.js +5 -2
  66. package/dist/esm/operation.js.map +1 -1
  67. package/dist/esm/patch.d.ts +2 -1
  68. package/dist/esm/patch.js.map +1 -1
  69. package/dist/esm/refs.d.ts +1 -1
  70. package/dist/esm/schema/children.d.ts +2 -0
  71. package/dist/esm/schema/children.js +20 -0
  72. package/dist/esm/schema/children.js.map +1 -0
  73. package/dist/esm/schema/fields.d.ts +3 -0
  74. package/dist/esm/schema/fields.js +30 -1
  75. package/dist/esm/schema/fields.js.map +1 -1
  76. package/dist/esm/schema/index.d.ts +1 -0
  77. package/dist/esm/schema/index.js +1 -0
  78. package/dist/esm/schema/index.js.map +1 -1
  79. package/dist/esm/schema/validation.d.ts +14 -3
  80. package/dist/esm/schema/validation.js +88 -30
  81. package/dist/esm/schema/validation.js.map +1 -1
  82. package/dist/esm/timestamp.d.ts +2 -1
  83. package/dist/esm/timestamp.js +4 -3
  84. package/dist/esm/timestamp.js.map +1 -1
  85. package/dist/esm/undo.js +3 -1
  86. package/dist/esm/undo.js.map +1 -1
  87. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  88. package/dist/tsconfig.tsbuildinfo +1 -1
  89. package/package.json +5 -5
  90. package/src/EventSubscriber.ts +3 -0
  91. package/src/batching.ts +7 -0
  92. package/src/index.ts +1 -0
  93. package/src/memo.test.ts +31 -0
  94. package/src/memo.ts +25 -0
  95. package/src/migration.ts +8 -0
  96. package/src/oids.ts +4 -0
  97. package/src/operation.ts +14 -5
  98. package/src/patch.ts +2 -1
  99. package/src/refs.ts +1 -1
  100. package/src/schema/children.ts +19 -0
  101. package/src/schema/fields.ts +27 -1
  102. package/src/schema/index.ts +1 -0
  103. package/src/schema/validation.ts +130 -45
  104. package/src/timestamp.ts +5 -3
  105. package/src/undo.ts +3 -1
@@ -1,97 +1,182 @@
1
- import { LEGACY_OID_KEY, OID_KEY } from '../oids.js';
2
1
  import { isObject } from '../utils.js';
3
- import { StorageCollectionSchema, StorageFieldSchema } from './types.js';
2
+ import { hasDefault, isNullable } from './fields.js';
3
+ import { StorageFieldSchema, StorageFieldsSchema } from './types.js';
4
4
 
5
5
  export function validateEntity(
6
- schema: StorageCollectionSchema,
6
+ schema: StorageFieldsSchema,
7
7
  entity: any,
8
- ): string | void {
8
+ ): EntityValidationProblem | void {
9
9
  for (const [key, value] of Object.entries(entity)) {
10
- if (!schema.fields[key]) {
11
- return `Invalid field "${key}"`;
10
+ if (!schema[key]) {
11
+ return {
12
+ type: 'invalid-key',
13
+ fieldPath: [key],
14
+ message: `Invalid field "${key}"`,
15
+ };
12
16
  }
13
17
  if (value) {
14
- const err = validateEntityField(schema.fields[key], value);
18
+ const err = validateEntityField({
19
+ field: schema[key],
20
+ value,
21
+ fieldPath: [key],
22
+ });
15
23
  if (err) return err;
16
24
  }
17
25
  }
18
26
  return;
19
27
  }
20
28
 
21
- export function validateEntityField(
22
- field: StorageFieldSchema,
23
- value: any,
24
- fieldPath: string[] = [],
25
- ): string | void {
29
+ export type EntityValidationProblem = {
30
+ type:
31
+ | 'null'
32
+ | 'no-default'
33
+ | 'invalid-type'
34
+ | 'invalid-value'
35
+ | 'invalid-key';
36
+ fieldPath: (string | number)[];
37
+ message: string;
38
+ };
39
+
40
+ export function validateEntityField({
41
+ field,
42
+ value,
43
+ fieldPath = [],
44
+ depth,
45
+ requireDefaults,
46
+ }: {
47
+ field: StorageFieldSchema;
48
+ value: any;
49
+ fieldPath: (string | number)[];
50
+ depth?: number;
51
+ requireDefaults?: boolean;
52
+ }): EntityValidationProblem | undefined {
53
+ if (depth !== undefined && depth <= 0) return;
54
+
55
+ if (isNullable(field) && value === null) return;
56
+ if (value === null) {
57
+ if (requireDefaults || !hasDefault(field)) {
58
+ return {
59
+ type: 'no-default',
60
+ fieldPath,
61
+ message: `Invalid null value for field ${formatField(fieldPath)}`,
62
+ };
63
+ }
64
+ }
65
+
26
66
  if (field.type === 'object') {
27
67
  if (!isObject(value)) {
28
- if (value === null && field.nullable) return;
29
- return `Expected object${
30
- field.nullable ? ' or null' : ''
31
- } for field ${formatField(fieldPath)}}`;
68
+ return {
69
+ type: 'invalid-type',
70
+ fieldPath,
71
+ message: `Expected object${
72
+ field.nullable ? ' or null' : ''
73
+ } for field ${formatField(fieldPath)}, got ${value}`,
74
+ };
32
75
  }
33
76
  for (const [key, subField] of Object.entries(field.properties)) {
34
77
  if (value[key]) {
35
- validateEntityField(subField, value[key]);
78
+ validateEntityField({
79
+ field: subField,
80
+ value: value[key],
81
+ fieldPath: [...fieldPath, key],
82
+ depth: depth !== undefined ? depth - 1 : undefined,
83
+ });
36
84
  }
37
85
  }
86
+ // check for unexpected keys
38
87
  for (const key of Object.keys(value)) {
39
88
  if (!field.properties[key]) {
40
- return `Invalid field "${key}" on value ${formatField(fieldPath)}`;
89
+ return {
90
+ type: 'invalid-key',
91
+ fieldPath: [...fieldPath, key],
92
+ message: `Invalid unexpected field "${key}" on value ${formatField(
93
+ fieldPath,
94
+ )}`,
95
+ };
41
96
  }
42
97
  }
43
98
  } else if (field.type === 'array') {
44
99
  if (!Array.isArray(value)) {
45
100
  if (value === null && field.nullable) return;
46
- return `Expected array${
47
- field.nullable ? ' or null' : ''
48
- } for field ${formatField(fieldPath)}}`;
101
+ return {
102
+ type: 'invalid-value',
103
+ fieldPath,
104
+ message: `Expected array${
105
+ field.nullable ? ' or null' : ''
106
+ } for field ${formatField(fieldPath)}, got ${value}`,
107
+ };
49
108
  }
50
109
  for (const item of value) {
51
- validateEntityField(field.items, item);
110
+ validateEntityField({
111
+ field: field.items,
112
+ value: item,
113
+ fieldPath: [...fieldPath, '[]'],
114
+ depth: depth !== undefined ? depth - 1 : undefined,
115
+ });
52
116
  }
53
117
  } else if (field.type === 'map') {
54
118
  if (!isObject(value)) {
55
- return `Expected map for field ${formatField(fieldPath)}}`;
119
+ return {
120
+ type: 'invalid-type',
121
+ fieldPath,
122
+ message: `Expected map for field ${formatField(
123
+ fieldPath,
124
+ )}, got ${value}`,
125
+ };
56
126
  }
57
127
  for (const [key, item] of Object.entries(value)) {
58
- // santiy check to weed out any id field
59
- if (key === OID_KEY || key === LEGACY_OID_KEY) continue;
60
- validateEntityField(field.values, item);
128
+ validateEntityField({
129
+ field: field.values,
130
+ value: item,
131
+ fieldPath: [...fieldPath, key],
132
+ depth: depth !== undefined ? depth - 1 : undefined,
133
+ });
61
134
  }
62
135
  } else if (field.type === 'string') {
63
136
  if (typeof value !== 'string') {
64
- if (value === null && field.nullable) return;
65
- return `Expected string ${
66
- field.nullable ? ' or null' : ''
67
- } for field ${formatField(fieldPath)}}`;
137
+ return {
138
+ type: 'invalid-type',
139
+ fieldPath,
140
+ message: `Expected string ${
141
+ field.nullable ? ' or null' : ''
142
+ } for field ${formatField(fieldPath)}, got ${value}`,
143
+ };
68
144
  }
69
145
  if (field.options && !field.options.includes(value)) {
70
- return `Expected one of ${field.options.join(
71
- ', ',
72
- )} for field ${formatField(fieldPath)}`;
146
+ return {
147
+ type: 'invalid-value',
148
+ fieldPath,
149
+ message: `Expected one of ${field.options.join(
150
+ ', ',
151
+ )} for field ${formatField(fieldPath)}, got ${value}`,
152
+ };
73
153
  }
74
154
  } else if (field.type === 'boolean') {
75
155
  if (typeof value !== 'boolean') {
76
- if (value === null && field.nullable) return;
77
- return `Expected boolean ${
78
- field.nullable ? ' or null' : ''
79
- } for field ${formatField(fieldPath)}}`;
156
+ return {
157
+ type: 'invalid-type',
158
+ fieldPath,
159
+ message: `Expected boolean ${
160
+ field.nullable ? ' or null' : ''
161
+ } for field ${formatField(fieldPath)}, got ${value}`,
162
+ };
80
163
  }
81
164
  } else if (field.type === 'number') {
82
165
  if (typeof value !== 'number') {
83
- if (value === null && field.nullable) return;
84
- return `Expected number ${
85
- field.nullable ? ' or null' : ''
86
- } for field ${formatField(fieldPath)}}`;
166
+ return {
167
+ type: 'invalid-type',
168
+ fieldPath,
169
+ message: `Expected number ${
170
+ field.nullable ? ' or null' : ''
171
+ } for field ${formatField(fieldPath)}, got ${value}`,
172
+ };
87
173
  }
88
174
  } else if (field.type === 'file') {
89
- if (value === null && !field.nullable) {
90
- return `Expected file for field ${formatField(fieldPath)}}`;
91
- }
175
+ // TODO: how?
92
176
  }
93
177
  }
94
178
 
95
- function formatField(fieldPath: string[]) {
179
+ function formatField(fieldPath: (string | number)[]) {
180
+ if (fieldPath.length === 0) return 'root';
96
181
  return fieldPath.join('.');
97
182
  }
package/src/timestamp.ts CHANGED
@@ -84,9 +84,7 @@ export class HybridLogicalClockTimestampProvider implements TimestampProvider {
84
84
  })
85
85
  );
86
86
  };
87
- getWallClockTime = (timestamp: string): number => {
88
- return deserializeHlcTimestamp(timestamp.slice(VERSION_BLOCK_LENGTH)).time;
89
- };
87
+ getWallClockTime = getWallClockTime;
90
88
  }
91
89
 
92
90
  class ClockDriftError extends Error {
@@ -290,3 +288,7 @@ export function getTimestampSchemaVersion(timestamp: string): number {
290
288
  export function compareTimestampSchemaVersions(a: string, b: string) {
291
289
  return getTimestampSchemaVersion(a) - getTimestampSchemaVersion(b);
292
290
  }
291
+
292
+ export function getWallClockTime(timestamp: string): number {
293
+ return deserializeHlcTimestamp(timestamp.slice(VERSION_BLOCK_LENGTH)).time;
294
+ }
package/src/undo.ts CHANGED
@@ -8,7 +8,7 @@ export function getUndoOperations(
8
8
  operations: Operation[],
9
9
  getNow: () => string,
10
10
  ): Operation[] {
11
- if (initial === undefined) {
11
+ if (initial === undefined || initial === null) {
12
12
  // if the initial state is nothing, then the undo is to delete everything.
13
13
  // there's nothing else to worry about!
14
14
  return [
@@ -199,6 +199,8 @@ function getUndoOperation(
199
199
  },
200
200
  },
201
201
  ];
202
+ case 'touch':
203
+ return [];
202
204
  default:
203
205
  throw new Error(`Cannot undo operation type: ${(data as any).op}`);
204
206
  }