@larc-iu/plaid-client 0.2.0-alpha.3 → 0.2.0-alpha.32

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 (73) hide show
  1. package/README.md +39 -0
  2. package/package.json +13 -6
  3. package/src/apiAnnotations.d.ts +508 -0
  4. package/src/apiAnnotations.js +543 -0
  5. package/src/apiDocuments.d.ts +569 -0
  6. package/src/apiDocuments.js +551 -0
  7. package/src/apiLayers.d.ts +369 -0
  8. package/src/apiLayers.js +222 -0
  9. package/src/apiMessages.d.ts +293 -0
  10. package/src/apiMessages.js +265 -0
  11. package/src/apiPeople.d.ts +626 -0
  12. package/src/apiPeople.js +698 -0
  13. package/src/apiProject.d.ts +761 -0
  14. package/src/apiProject.js +774 -0
  15. package/src/apiServer.d.ts +457 -0
  16. package/src/apiServer.js +424 -0
  17. package/src/apiVocab.d.ts +456 -0
  18. package/src/apiVocab.js +425 -0
  19. package/src/batch.d.ts +54 -0
  20. package/src/batch.js +323 -0
  21. package/src/batchRef.d.ts +30 -0
  22. package/src/batchRef.js +128 -0
  23. package/src/client.d.ts +1321 -0
  24. package/src/client.js +743 -0
  25. package/src/codepoint.d.ts +94 -0
  26. package/src/codepoint.js +131 -1
  27. package/src/constraints.d.ts +85 -0
  28. package/src/constraints.js +112 -0
  29. package/src/created.d.ts +2 -0
  30. package/src/created.js +29 -0
  31. package/src/documentLock.d.ts +172 -0
  32. package/src/documentLock.js +384 -0
  33. package/src/entryForms.d.ts +68 -0
  34. package/src/entryForms.js +164 -0
  35. package/src/events.d.ts +60 -0
  36. package/src/events.js +271 -0
  37. package/src/families.d.ts +349 -0
  38. package/src/families.js +396 -0
  39. package/src/http.d.ts +233 -0
  40. package/src/http.js +809 -93
  41. package/src/ids.d.ts +26 -0
  42. package/src/ids.js +96 -0
  43. package/src/index.d.ts +101 -0
  44. package/src/index.js +183 -2105
  45. package/src/metadataOps.d.ts +15 -0
  46. package/src/metadataOps.js +124 -0
  47. package/src/operations.d.ts +36 -0
  48. package/src/operations.js +164 -0
  49. package/src/pagination.d.ts +49 -0
  50. package/src/pagination.js +20 -53
  51. package/src/provenance.d.ts +243 -0
  52. package/src/provenance.js +458 -36
  53. package/src/replayed.d.ts +3 -0
  54. package/src/replayed.js +31 -0
  55. package/src/requester.d.ts +60 -0
  56. package/src/requester.js +74 -0
  57. package/src/roles.d.ts +104 -0
  58. package/src/roles.js +64 -2
  59. package/src/serviceSchema.d.ts +337 -0
  60. package/src/serviceSchema.js +177 -60
  61. package/src/services.d.ts +319 -0
  62. package/src/services.js +742 -58
  63. package/src/sse.d.ts +30 -0
  64. package/src/sse.js +80 -3
  65. package/src/testing.d.ts +128 -0
  66. package/src/testing.js +379 -0
  67. package/src/textEdits.d.ts +59 -0
  68. package/src/textEdits.js +234 -0
  69. package/src/transforms.d.ts +25 -0
  70. package/src/transforms.js +11 -1
  71. package/src/types.d.ts +867 -0
  72. package/src/types.js +663 -0
  73. package/index.d.ts +0 -532
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The code points of `body`, as a frozen array shared with other callers.
3
+ * @param {string} body
4
+ * @returns {readonly string[]}
5
+ */
6
+ export function bodyCodePoints(body: string): readonly string[];
7
+ /**
8
+ * The net change of `ops` applied in turn to `body`, as gaps in the old
9
+ * body's code points: sorted, and never touching (at least one old code point
10
+ * is kept between two gaps). Old text deleted and typed back is typed text,
11
+ * and a gap whose value is the old text it removes is dropped. Throws on a
12
+ * malformed or out-of-bounds op.
13
+ * @param {string} body
14
+ * @param {TextEditOp[]} ops
15
+ * @returns {TextGap[]}
16
+ */
17
+ export function composeTextEdits(body: string, ops: TextEditOp[]): TextGap[];
18
+ /**
19
+ * Gaps (old-body coordinates, sorted, not overlapping) as running ops: an
20
+ * insert where a gap removes nothing, a delete where it types nothing, and a
21
+ * replace otherwise.
22
+ * @param {TextGap[]} gaps
23
+ * @returns {TextEditOp[]}
24
+ */
25
+ export function gapsToOps(gaps: TextGap[]): TextEditOp[];
26
+ /**
27
+ * `body` with `ops` applied in turn (running coordinates, code points).
28
+ * Throws on a malformed or out-of-bounds op.
29
+ * @param {string} body
30
+ * @param {TextEditOp[]} ops
31
+ * @returns {string}
32
+ */
33
+ export function applyTextOps(body: string, ops: TextEditOp[]): string;
34
+ /**
35
+ * One op as `PATCH /texts/:id` takes it. Each index is in the body the ops
36
+ * before it left.
37
+ */
38
+ export type TextEditOp = {
39
+ type: "insert";
40
+ index: number;
41
+ value: string;
42
+ } | {
43
+ type: "delete";
44
+ index: number;
45
+ value: number;
46
+ } | {
47
+ type: "replace";
48
+ index: number;
49
+ length: number;
50
+ value: string;
51
+ };
52
+ /**
53
+ * Old-body code points [start, end) give way to `value`.
54
+ */
55
+ export type TextGap = {
56
+ start: number;
57
+ end: number;
58
+ value: string;
59
+ };
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Text edit operations: composing a stream of ops into the net change.
3
+ *
4
+ * An op is one of the shapes `PATCH /texts/:id` takes:
5
+ *
6
+ * { type: 'insert', index, value: string }
7
+ * { type: 'delete', index, value: count }
8
+ * { type: 'replace', index, length, value: string }
9
+ *
10
+ * Every index and count is in Unicode CODE POINTS (see ./codepoint.js), and
11
+ * the ops are in running coordinates: each op's `index` is in the body as the
12
+ * ops before it left it.
13
+ *
14
+ * A GAP is `{ start, end, value }` in the OLD body's coordinates: old
15
+ * [start, end) gives way to `value`. `composeTextEdits` turns any op stream
16
+ * into gaps that depend only on the net change, never on how the keystrokes
17
+ * came, which is also how the server reads an edit. The Python client's
18
+ * `plaid_client.text_edits` is the twin of this module.
19
+ */
20
+
21
+ /**
22
+ * One op as `PATCH /texts/:id` takes it. Each index is in the body the ops
23
+ * before it left.
24
+ * @typedef {{type: "insert", index: number, value: string}
25
+ * | {type: "delete", index: number, value: number}
26
+ * | {type: "replace", index: number, length: number, value: string}} TextEditOp
27
+ */
28
+
29
+ /**
30
+ * Old-body code points [start, end) give way to `value`.
31
+ * @typedef {{start: number, end: number, value: string}} TextGap
32
+ */
33
+
34
+ const codePoints = (s) => [...s];
35
+
36
+ // The last body split into code points, kept: an edit log composes the same
37
+ // long body once for each of its kept states, and splitting it each time made
38
+ // a save on a long text block the page (H31-TEXT-1). Frozen, as every caller
39
+ // shares it.
40
+ let lastBody = null;
41
+ let lastPoints = Object.freeze([]);
42
+
43
+ /**
44
+ * The code points of `body`, as a frozen array shared with other callers.
45
+ * @param {string} body
46
+ * @returns {readonly string[]}
47
+ */
48
+ export function bodyCodePoints(body) {
49
+ if (body !== lastBody) {
50
+ lastPoints = Object.freeze(codePoints(body));
51
+ lastBody = body;
52
+ }
53
+ return lastPoints;
54
+ }
55
+
56
+ // The op as a refusal names it.
57
+ const opJson = (op) => String(JSON.stringify(op) ?? 'null');
58
+
59
+ /**
60
+ * The op's type after checking its shape and bounds against a body of `len`
61
+ * code points, as the server checks it. Throws an Error otherwise.
62
+ */
63
+ function checkOp(op, len) {
64
+ const type = op && op.type;
65
+ const { index, value, length } = op || {};
66
+ const int = Number.isInteger;
67
+ const wellFormed =
68
+ (type === 'insert' && int(index) && typeof value === 'string') ||
69
+ (type === 'delete' && int(index) && int(value)) ||
70
+ (type === 'replace' && int(index) && int(length) && typeof value === 'string');
71
+ if (!wellFormed) {
72
+ throw new Error(
73
+ `Malformed text edit operation: ${opJson(op)}. Expected {type: "insert", index: int, ` +
74
+ `value: string}, {type: "delete", index: int, value: int} or {type: "replace", ` +
75
+ `index: int, length: int, value: string}`,
76
+ );
77
+ }
78
+ if (!(op.side == null || op.side === 'before' || op.side === 'after')) {
79
+ throw new Error(`A text edit's side is "before" or "after": ${opJson(op)}`);
80
+ }
81
+ const inBounds =
82
+ type === 'insert'
83
+ ? index >= 0 && index <= len
84
+ : type === 'delete'
85
+ ? index >= 0 && value >= 0 && index + value <= len
86
+ : index >= 0 && length >= 0 && index + length <= len;
87
+ if (!inBounds) {
88
+ throw new Error(
89
+ `Text edit operation out of bounds: ${opJson(op)} (text length is ${len} code points)`,
90
+ );
91
+ }
92
+ return type;
93
+ }
94
+
95
+ // How many code points an op removes and what it types.
96
+ function opParts(type, op) {
97
+ if (type === 'insert') return [0, op.value];
98
+ if (type === 'delete') return [op.value, ''];
99
+ return [op.length, op.value];
100
+ }
101
+
102
+ const segLength = (seg) => (seg.old ? seg.end - seg.start : seg.chars.length);
103
+
104
+ // The segment list of the new text, cut so that a segment boundary falls at
105
+ // code point `at`. Returns the index of the first segment at or after `at`.
106
+ function cutAt(segs, at) {
107
+ let pos = 0;
108
+ for (let i = 0; i < segs.length; i += 1) {
109
+ const seg = segs[i];
110
+ const n = segLength(seg);
111
+ if (at === pos) return i;
112
+ if (at < pos + n) {
113
+ const k = at - pos;
114
+ const left = seg.old
115
+ ? { old: true, start: seg.start, end: seg.start + k }
116
+ : { old: false, chars: seg.chars.slice(0, k) };
117
+ const right = seg.old
118
+ ? { old: true, start: seg.start + k, end: seg.end }
119
+ : { old: false, chars: seg.chars.slice(k) };
120
+ segs.splice(i, 1, left, right);
121
+ return i + 1;
122
+ }
123
+ pos += n;
124
+ }
125
+ return segs.length;
126
+ }
127
+
128
+ /**
129
+ * The net change of `ops` applied in turn to `body`, as gaps in the old
130
+ * body's code points: sorted, and never touching (at least one old code point
131
+ * is kept between two gaps). Old text deleted and typed back is typed text,
132
+ * and a gap whose value is the old text it removes is dropped. Throws on a
133
+ * malformed or out-of-bounds op.
134
+ * @param {string} body
135
+ * @param {TextEditOp[]} ops
136
+ * @returns {TextGap[]}
137
+ */
138
+ export function composeTextEdits(body, ops) {
139
+ const old = bodyCodePoints(body);
140
+ let segs = old.length ? [{ old: true, start: 0, end: old.length }] : [];
141
+ let len = old.length;
142
+ for (const op of ops) {
143
+ const type = checkOp(op, len);
144
+ const [del, typed] = opParts(type, op);
145
+ const chars = codePoints(typed);
146
+ const from = cutAt(segs, op.index);
147
+ const to = cutAt(segs, op.index + del);
148
+ const inserted = chars.length ? [{ old: false, chars }] : [];
149
+ segs.splice(from, to - from, ...inserted);
150
+ len += chars.length - del;
151
+ segs = mergeTyped(segs);
152
+ }
153
+
154
+ const gaps = [];
155
+ let kept = 0; // the old position the last kept run ended at
156
+ let typed = [];
157
+ const close = (next) => {
158
+ if (next > kept || typed.length) {
159
+ const value = typed.join('');
160
+ if (value !== old.slice(kept, next).join('')) gaps.push({ start: kept, end: next, value });
161
+ }
162
+ typed = [];
163
+ };
164
+ for (const seg of segs) {
165
+ if (seg.old) {
166
+ close(seg.start);
167
+ kept = seg.end;
168
+ } else {
169
+ for (const c of seg.chars) typed.push(c);
170
+ }
171
+ }
172
+ close(old.length);
173
+ return gaps;
174
+ }
175
+
176
+ // Neighbouring typed segments as one, and neighbouring old ranges as one.
177
+ function mergeTyped(segs) {
178
+ const out = [];
179
+ for (const seg of segs) {
180
+ const last = out[out.length - 1];
181
+ if (last && !last.old && !seg.old) {
182
+ out[out.length - 1] = { old: false, chars: last.chars.concat(seg.chars) };
183
+ } else if (last && last.old && seg.old && last.end === seg.start) {
184
+ out[out.length - 1] = { old: true, start: last.start, end: seg.end };
185
+ } else if (segLength(seg) > 0) {
186
+ out.push(seg);
187
+ }
188
+ }
189
+ return out;
190
+ }
191
+
192
+ /**
193
+ * Gaps (old-body coordinates, sorted, not overlapping) as running ops: an
194
+ * insert where a gap removes nothing, a delete where it types nothing, and a
195
+ * replace otherwise.
196
+ * @param {TextGap[]} gaps
197
+ * @returns {TextEditOp[]}
198
+ */
199
+ export function gapsToOps(gaps) {
200
+ const ops = [];
201
+ let shift = 0;
202
+ for (const { start, end, value } of gaps) {
203
+ const index = start + shift;
204
+ const typed = codePoints(value).length;
205
+ if (start === end) {
206
+ if (typed) ops.push({ type: 'insert', index, value });
207
+ } else if (!typed) {
208
+ ops.push({ type: 'delete', index, value: end - start });
209
+ } else {
210
+ ops.push({ type: 'replace', index, length: end - start, value });
211
+ }
212
+ shift += typed - (end - start);
213
+ }
214
+ return ops;
215
+ }
216
+
217
+ /**
218
+ * `body` with `ops` applied in turn (running coordinates, code points).
219
+ * Throws on a malformed or out-of-bounds op.
220
+ * @param {string} body
221
+ * @param {TextEditOp[]} ops
222
+ * @returns {string}
223
+ */
224
+ export function applyTextOps(body, ops) {
225
+ let chars = codePoints(body);
226
+ for (const op of ops) {
227
+ const type = checkOp(op, chars.length);
228
+ const [del, typed] = opParts(type, op);
229
+ chars = chars
230
+ .slice(0, op.index)
231
+ .concat(codePoints(typed), chars.slice(op.index + del));
232
+ }
233
+ return chars.join('');
234
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Convert kebab-case/namespaced key to camelCase.
3
+ * 'layer-id' -> 'layerId'
4
+ * 'relation/layer' -> 'layer' (namespace stripped)
5
+ * Hyphens before a digit are also consumed ('layer-2' -> 'layer2') so no stray
6
+ * hyphen is ever left in the key. (The Python client uses snake_case, where the
7
+ * analogous key is 'layer_2' — the local spelling differs by convention, but
8
+ * neither leaves a separator that doesn't belong to the convention.)
9
+ */
10
+ export function transformKeyToCamel(key: any): any;
11
+ /**
12
+ * Convert camelCase key to kebab-case.
13
+ * 'layerId' -> 'layer-id'
14
+ */
15
+ export function transformKeyFromCamel(key: any): any;
16
+ /**
17
+ * Recursively transform request object keys from camelCase to kebab-case.
18
+ * Preserves `metadata` and `config` contents without transformation.
19
+ */
20
+ export function transformRequest(obj: any): any;
21
+ /**
22
+ * Recursively transform response object keys from kebab-case/namespaced to camelCase.
23
+ * Preserves `metadata` and `config` contents without transformation.
24
+ */
25
+ export function transformResponse(obj: any): any;
package/src/transforms.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { BatchRef } from "./batchRef.js";
2
+
1
3
  /**
2
4
  * Convert kebab-case/namespaced key to camelCase.
3
5
  * 'layer-id' -> 'layerId'
@@ -27,19 +29,27 @@ export function transformKeyFromCamel(key) {
27
29
  // usual case conversion.
28
30
  const OPAQUE_KEYS = new Set(['metadata', 'config', 'bindings']);
29
31
 
32
+ // A request's `metadata` may also be a list of metadata ops (a bulk update
33
+ // entry), whose values are user data too, so it passes through verbatim.
34
+ const isOpaque = (key, value) =>
35
+ typeof value === 'object' && value !== null &&
36
+ (Array.isArray(value) ? key === 'metadata' : OPAQUE_KEYS.has(key));
37
+
30
38
  /**
31
39
  * Recursively transform request object keys from camelCase to kebab-case.
32
40
  * Preserves `metadata` and `config` contents without transformation.
33
41
  */
34
42
  export function transformRequest(obj) {
35
43
  if (obj === null || obj === undefined) return obj;
44
+ // A batch ref is taken out whole when its write is queued.
45
+ if (obj instanceof BatchRef) return obj;
36
46
  if (Array.isArray(obj)) return obj.map(item => transformRequest(item));
37
47
  if (typeof obj !== 'object') return obj;
38
48
 
39
49
  const transformed = {};
40
50
  for (const [key, value] of Object.entries(obj)) {
41
51
  const newKey = transformKeyFromCamel(key);
42
- if (OPAQUE_KEYS.has(key) && typeof value === 'object' && value !== null && !Array.isArray(value)) {
52
+ if (isOpaque(key, value)) {
43
53
  transformed[newKey] = value;
44
54
  } else {
45
55
  transformed[newKey] = transformRequest(value);