@warp-drive/schema-record 0.0.0-alpha.101
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/LICENSE.md +12 -0
- package/NCC-1701-a-blue.svg +4 -0
- package/NCC-1701-a.svg +4 -0
- package/README.md +265 -0
- package/addon-main.cjs +5 -0
- package/dist/hooks.js +19 -0
- package/dist/hooks.js.map +1 -0
- package/dist/record.js +1159 -0
- package/dist/record.js.map +1 -0
- package/dist/schema.js +278 -0
- package/dist/schema.js.map +1 -0
- package/dist/symbols-DqoS4ybV.js +47 -0
- package/dist/symbols-DqoS4ybV.js.map +1 -0
- package/package.json +111 -0
- package/unstable-preview-types/-private/compute.d.ts +41 -0
- package/unstable-preview-types/-private/compute.d.ts.map +1 -0
- package/unstable-preview-types/-private/managed-array.d.ts +25 -0
- package/unstable-preview-types/-private/managed-array.d.ts.map +1 -0
- package/unstable-preview-types/-private/managed-object.d.ts +23 -0
- package/unstable-preview-types/-private/managed-object.d.ts.map +1 -0
- package/unstable-preview-types/hooks.d.ts +8 -0
- package/unstable-preview-types/hooks.d.ts.map +1 -0
- package/unstable-preview-types/index.d.ts +7 -0
- package/unstable-preview-types/record.d.ts +27 -0
- package/unstable-preview-types/record.d.ts.map +1 -0
- package/unstable-preview-types/schema.d.ts +85 -0
- package/unstable-preview-types/schema.d.ts.map +1 -0
- package/unstable-preview-types/symbols.d.ts +16 -0
- package/unstable-preview-types/symbols.d.ts.map +1 -0
package/dist/record.js
ADDED
|
@@ -0,0 +1,1159 @@
|
|
|
1
|
+
import { macroCondition, getGlobalConfig, dependencySatisfies, importSync } from '@embroider/macros';
|
|
2
|
+
import { setRecordIdentifier, recordIdentifierFor } from '@ember-data/store/-private';
|
|
3
|
+
import { createSignal, subscribe, defineSignal, peekSignal, getSignal, Signals, entangleSignal, addToTransaction } from '@ember-data/tracking/-private';
|
|
4
|
+
import { STRUCTURED } from '@warp-drive/core-types/request';
|
|
5
|
+
import { RecordStore } from '@warp-drive/core-types/symbols';
|
|
6
|
+
import { getOrSetGlobal } from '@warp-drive/core-types/-private';
|
|
7
|
+
import { S as SOURCE, A as ARRAY_SIGNAL, E as Editable, L as Legacy, I as Identifier, P as Parent, O as OBJECT_SIGNAL, a as EmbeddedPath, D as Destroy, C as Checkout, b as EmbeddedType } from "./symbols-DqoS4ybV.js";
|
|
8
|
+
const ARRAY_GETTER_METHODS = new Set([Symbol.iterator, 'concat', 'entries', 'every', 'fill', 'filter', 'find', 'findIndex', 'flat', 'flatMap', 'forEach', 'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'map', 'reduce', 'reduceRight', 'slice', 'some', 'values']);
|
|
9
|
+
// const ARRAY_SETTER_METHODS = new Set<KeyType>(['push', 'pop', 'unshift', 'shift', 'splice', 'sort']);
|
|
10
|
+
const SYNC_PROPS = new Set(['[]', 'length']);
|
|
11
|
+
function isArrayGetter(prop) {
|
|
12
|
+
return ARRAY_GETTER_METHODS.has(prop);
|
|
13
|
+
}
|
|
14
|
+
const ARRAY_SETTER_METHODS = new Set(['push', 'pop', 'unshift', 'shift', 'splice', 'sort']);
|
|
15
|
+
function isArraySetter(prop) {
|
|
16
|
+
return ARRAY_SETTER_METHODS.has(prop);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// function isSelfProp<T extends object>(self: T, prop: KeyType): prop is keyof T {
|
|
20
|
+
// return prop in self;
|
|
21
|
+
// }
|
|
22
|
+
|
|
23
|
+
function convertToInt(prop) {
|
|
24
|
+
if (typeof prop === 'symbol') return null;
|
|
25
|
+
const num = Number(prop);
|
|
26
|
+
if (isNaN(num)) return null;
|
|
27
|
+
return num % 1 === 0 ? num : null;
|
|
28
|
+
}
|
|
29
|
+
function safeForEach(instance, arr, store, callback, target) {
|
|
30
|
+
if (target === undefined) {
|
|
31
|
+
target = null;
|
|
32
|
+
}
|
|
33
|
+
// clone to prevent mutation
|
|
34
|
+
arr = arr.slice();
|
|
35
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
36
|
+
if (!test) {
|
|
37
|
+
throw new Error('`forEach` expects a function as first argument.');
|
|
38
|
+
}
|
|
39
|
+
})(typeof callback === 'function') : {};
|
|
40
|
+
|
|
41
|
+
// because we retrieveLatest above we need not worry if array is mutated during iteration
|
|
42
|
+
// by unloadRecord/rollbackAttributes
|
|
43
|
+
// push/add/removeObject may still be problematic
|
|
44
|
+
// but this is a more traditionally expected forEach bug.
|
|
45
|
+
const length = arr.length; // we need to access length to ensure we are consumed
|
|
46
|
+
|
|
47
|
+
for (let index = 0; index < length; index++) {
|
|
48
|
+
callback.call(target, arr[index], index, instance);
|
|
49
|
+
}
|
|
50
|
+
return instance;
|
|
51
|
+
}
|
|
52
|
+
class ManagedArray {
|
|
53
|
+
[SOURCE];
|
|
54
|
+
constructor(store, schema, cache, field, data, identifier, path, owner, isSchemaArray, editable, legacy) {
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
56
|
+
const self = this;
|
|
57
|
+
this[SOURCE] = data?.slice();
|
|
58
|
+
this[ARRAY_SIGNAL] = createSignal(this, 'length');
|
|
59
|
+
const IS_EDITABLE = this[Editable] = editable ?? false;
|
|
60
|
+
this[Legacy] = legacy;
|
|
61
|
+
const _SIGNAL = this[ARRAY_SIGNAL];
|
|
62
|
+
const boundFns = new Map();
|
|
63
|
+
this.identifier = identifier;
|
|
64
|
+
this.path = path;
|
|
65
|
+
this.owner = owner;
|
|
66
|
+
let transaction = false;
|
|
67
|
+
const mode = field.options?.key ?? '@identity';
|
|
68
|
+
const RefStorage = mode === '@identity' ? WeakMap :
|
|
69
|
+
// CAUTION CAUTION CAUTION
|
|
70
|
+
// this is a pile of lies
|
|
71
|
+
// the Map is Map<string, WeakRef<SchemaRecord>>
|
|
72
|
+
// but TS does not understand how to juggle modes like this
|
|
73
|
+
// internal to a method like ours without us duplicating the code
|
|
74
|
+
// into two separate methods.
|
|
75
|
+
Map;
|
|
76
|
+
const ManagedRecordRefs = isSchemaArray ? new RefStorage() : null;
|
|
77
|
+
const proxy = new Proxy(this[SOURCE], {
|
|
78
|
+
get(target, prop, receiver) {
|
|
79
|
+
if (prop === ARRAY_SIGNAL) {
|
|
80
|
+
return _SIGNAL;
|
|
81
|
+
}
|
|
82
|
+
if (prop === 'identifier') {
|
|
83
|
+
return self.identifier;
|
|
84
|
+
}
|
|
85
|
+
if (prop === 'owner') {
|
|
86
|
+
return self.owner;
|
|
87
|
+
}
|
|
88
|
+
const index = convertToInt(prop);
|
|
89
|
+
if (_SIGNAL.shouldReset && (index !== null || SYNC_PROPS.has(prop) || isArrayGetter(prop))) {
|
|
90
|
+
_SIGNAL.t = false;
|
|
91
|
+
_SIGNAL.shouldReset = false;
|
|
92
|
+
const newData = cache.getAttr(identifier, path);
|
|
93
|
+
if (newData && newData !== self[SOURCE]) {
|
|
94
|
+
self[SOURCE].length = 0;
|
|
95
|
+
self[SOURCE].push(...newData);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (index !== null) {
|
|
99
|
+
let val;
|
|
100
|
+
if (mode === '@hash') {
|
|
101
|
+
val = target[index];
|
|
102
|
+
const hashField = schema.resource({
|
|
103
|
+
type: field.type
|
|
104
|
+
}).identity;
|
|
105
|
+
const hashFn = schema.hashFn(hashField);
|
|
106
|
+
val = hashFn(val, null, null);
|
|
107
|
+
} else {
|
|
108
|
+
// if mode is not @identity or @index, then access the key path.
|
|
109
|
+
// we should assert that `mode` is a string
|
|
110
|
+
// it should read directly from the cache value for that field (e.g. no derivation, no transformation)
|
|
111
|
+
// and, we likely should lookup the associated field and throw an error IF
|
|
112
|
+
// the given field does not exist OR
|
|
113
|
+
// the field is anything other than a GenericField or LegacyAttributeField.
|
|
114
|
+
if (mode !== '@identity' && mode !== '@index') {
|
|
115
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
116
|
+
if (!test) {
|
|
117
|
+
throw new Error('mode must be a string');
|
|
118
|
+
}
|
|
119
|
+
})(typeof mode === 'string') : {};
|
|
120
|
+
const modeField = schema.resource({
|
|
121
|
+
type: field.type
|
|
122
|
+
}).fields.find(f => f.name === mode);
|
|
123
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
124
|
+
if (!test) {
|
|
125
|
+
throw new Error('field must exist in schema');
|
|
126
|
+
}
|
|
127
|
+
})(modeField) : {};
|
|
128
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
129
|
+
if (!test) {
|
|
130
|
+
throw new Error('field must be a GenericField or LegacyAttributeField');
|
|
131
|
+
}
|
|
132
|
+
})(modeField.kind === 'field' || modeField.kind === 'attribute') : {};
|
|
133
|
+
}
|
|
134
|
+
val = mode === '@identity' ? target[index] : mode === '@index' ? '@index' : target[index][mode];
|
|
135
|
+
}
|
|
136
|
+
if (isSchemaArray) {
|
|
137
|
+
if (!transaction) {
|
|
138
|
+
subscribe(_SIGNAL);
|
|
139
|
+
}
|
|
140
|
+
if (val) {
|
|
141
|
+
const recordRef = ManagedRecordRefs.get(val);
|
|
142
|
+
let record = recordRef?.deref();
|
|
143
|
+
if (!record) {
|
|
144
|
+
const recordPath = path.slice();
|
|
145
|
+
// this is a dirty lie since path is string[] but really we
|
|
146
|
+
// should change the types for paths to `Array<string | number>`
|
|
147
|
+
// TODO we should allow the schema for the field to define a "key"
|
|
148
|
+
// for stability. Default should be `@identity` which means that
|
|
149
|
+
// same object reference from cache should result in same SchemaRecord
|
|
150
|
+
// embedded object.
|
|
151
|
+
recordPath.push(index);
|
|
152
|
+
const recordIdentifier = self.owner[Identifier] || self.owner[Parent];
|
|
153
|
+
record = new SchemaRecord(store, recordIdentifier, {
|
|
154
|
+
[Editable]: self.owner[Editable],
|
|
155
|
+
[Legacy]: self.owner[Legacy]
|
|
156
|
+
}, true, field.type, recordPath);
|
|
157
|
+
// if mode is not @identity or @index, then access the key path now
|
|
158
|
+
// to determine the key value.
|
|
159
|
+
// chris says we can implement this as a special kind `@hash` which
|
|
160
|
+
// would be a function that only has access to the cache value and not
|
|
161
|
+
// the record itself, so derivation is possible but intentionally limited
|
|
162
|
+
// and non-reactive?
|
|
163
|
+
ManagedRecordRefs.set(val, new WeakRef(record));
|
|
164
|
+
}
|
|
165
|
+
return record;
|
|
166
|
+
}
|
|
167
|
+
return val;
|
|
168
|
+
}
|
|
169
|
+
if (!transaction) {
|
|
170
|
+
subscribe(_SIGNAL);
|
|
171
|
+
}
|
|
172
|
+
if (field.type) {
|
|
173
|
+
const transform = schema.transformation(field);
|
|
174
|
+
return transform.hydrate(val, field.options ?? null, self.owner);
|
|
175
|
+
}
|
|
176
|
+
return val;
|
|
177
|
+
}
|
|
178
|
+
if (isArrayGetter(prop)) {
|
|
179
|
+
let fn = boundFns.get(prop);
|
|
180
|
+
if (fn === undefined) {
|
|
181
|
+
if (prop === 'forEach') {
|
|
182
|
+
fn = function () {
|
|
183
|
+
subscribe(_SIGNAL);
|
|
184
|
+
transaction = true;
|
|
185
|
+
const result = safeForEach(receiver, target, store, arguments[0], arguments[1]);
|
|
186
|
+
transaction = false;
|
|
187
|
+
return result;
|
|
188
|
+
};
|
|
189
|
+
} else {
|
|
190
|
+
fn = function () {
|
|
191
|
+
subscribe(_SIGNAL);
|
|
192
|
+
// array functions must run through Reflect to work properly
|
|
193
|
+
// binding via other means will not work.
|
|
194
|
+
transaction = true;
|
|
195
|
+
const result = Reflect.apply(target[prop], receiver, arguments);
|
|
196
|
+
transaction = false;
|
|
197
|
+
return result;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
boundFns.set(prop, fn);
|
|
201
|
+
}
|
|
202
|
+
return fn;
|
|
203
|
+
}
|
|
204
|
+
if (isArraySetter(prop)) {
|
|
205
|
+
let fn = boundFns.get(prop);
|
|
206
|
+
if (fn === undefined) {
|
|
207
|
+
fn = function () {
|
|
208
|
+
if (!IS_EDITABLE) {
|
|
209
|
+
throw new Error(`Mutating this array via ${String(prop)} is not allowed because the record is not editable`);
|
|
210
|
+
}
|
|
211
|
+
subscribe(_SIGNAL);
|
|
212
|
+
transaction = true;
|
|
213
|
+
const result = Reflect.apply(target[prop], receiver, arguments);
|
|
214
|
+
transaction = false;
|
|
215
|
+
return result;
|
|
216
|
+
};
|
|
217
|
+
boundFns.set(prop, fn);
|
|
218
|
+
}
|
|
219
|
+
return fn;
|
|
220
|
+
}
|
|
221
|
+
return Reflect.get(target, prop, receiver);
|
|
222
|
+
},
|
|
223
|
+
set(target, prop, value, receiver) {
|
|
224
|
+
if (!IS_EDITABLE) {
|
|
225
|
+
let errorPath = identifier.type;
|
|
226
|
+
if (path) {
|
|
227
|
+
errorPath = path[path.length - 1];
|
|
228
|
+
}
|
|
229
|
+
throw new Error(`Cannot set ${String(prop)} on ${errorPath} because the record is not editable`);
|
|
230
|
+
}
|
|
231
|
+
if (prop === 'identifier') {
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
233
|
+
self.identifier = value;
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
if (prop === 'owner') {
|
|
237
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
238
|
+
self.owner = value;
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
const reflect = Reflect.set(target, prop, value, receiver);
|
|
242
|
+
if (reflect) {
|
|
243
|
+
if (!field.type) {
|
|
244
|
+
cache.setAttr(identifier, path, self[SOURCE]);
|
|
245
|
+
_SIGNAL.shouldReset = true;
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
let rawValue = self[SOURCE];
|
|
249
|
+
if (!isSchemaArray) {
|
|
250
|
+
const transform = schema.transformation(field);
|
|
251
|
+
if (!transform) {
|
|
252
|
+
throw new Error(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`);
|
|
253
|
+
}
|
|
254
|
+
rawValue = self[SOURCE].map(item => transform.serialize(item, field.options ?? null, self.owner));
|
|
255
|
+
}
|
|
256
|
+
cache.setAttr(identifier, path, rawValue);
|
|
257
|
+
_SIGNAL.shouldReset = true;
|
|
258
|
+
}
|
|
259
|
+
return reflect;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
return proxy;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const ObjectSymbols = new Set([OBJECT_SIGNAL, Parent, SOURCE, Editable, EmbeddedPath]);
|
|
266
|
+
|
|
267
|
+
// const ignoredGlobalFields = new Set<string>(['setInterval', 'nodeType', 'nodeName', 'length', 'document', STRUCTURED]);
|
|
268
|
+
|
|
269
|
+
class ManagedObject {
|
|
270
|
+
constructor(schema, cache, field, data, identifier, path, owner, editable, legacy) {
|
|
271
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
272
|
+
const self = this;
|
|
273
|
+
this[SOURCE] = {
|
|
274
|
+
...data
|
|
275
|
+
};
|
|
276
|
+
this[OBJECT_SIGNAL] = createSignal(this, 'length');
|
|
277
|
+
this[Editable] = editable;
|
|
278
|
+
this[Legacy] = legacy;
|
|
279
|
+
this[Parent] = identifier;
|
|
280
|
+
this[EmbeddedPath] = path;
|
|
281
|
+
const _SIGNAL = this[OBJECT_SIGNAL];
|
|
282
|
+
const proxy = new Proxy(this[SOURCE], {
|
|
283
|
+
ownKeys() {
|
|
284
|
+
return Object.keys(self[SOURCE]);
|
|
285
|
+
},
|
|
286
|
+
has(target, prop) {
|
|
287
|
+
return prop in self[SOURCE];
|
|
288
|
+
},
|
|
289
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
290
|
+
return {
|
|
291
|
+
writable: editable,
|
|
292
|
+
enumerable: true,
|
|
293
|
+
configurable: true
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
get(target, prop, receiver) {
|
|
297
|
+
if (ObjectSymbols.has(prop)) {
|
|
298
|
+
return self[prop];
|
|
299
|
+
}
|
|
300
|
+
if (prop === Symbol.toPrimitive) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
if (prop === Symbol.toStringTag) {
|
|
304
|
+
return `ManagedObject<${identifier.type}:${identifier.id} (${identifier.lid})>`;
|
|
305
|
+
}
|
|
306
|
+
if (prop === 'constructor') {
|
|
307
|
+
return Object;
|
|
308
|
+
}
|
|
309
|
+
if (prop === 'toString') {
|
|
310
|
+
return function () {
|
|
311
|
+
return `ManagedObject<${identifier.type}:${identifier.id} (${identifier.lid})>`;
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (prop === 'toHTML') {
|
|
315
|
+
return function () {
|
|
316
|
+
return '<div>ManagedObject</div>';
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (_SIGNAL.shouldReset) {
|
|
320
|
+
_SIGNAL.t = false;
|
|
321
|
+
_SIGNAL.shouldReset = false;
|
|
322
|
+
let newData = cache.getAttr(identifier, path);
|
|
323
|
+
if (newData && newData !== self[SOURCE]) {
|
|
324
|
+
if (field.type) {
|
|
325
|
+
const transform = schema.transformation(field);
|
|
326
|
+
newData = transform.hydrate(newData, field.options ?? null, owner);
|
|
327
|
+
}
|
|
328
|
+
self[SOURCE] = {
|
|
329
|
+
...newData
|
|
330
|
+
}; // Add type assertion for newData
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (prop in self[SOURCE]) {
|
|
334
|
+
subscribe(_SIGNAL);
|
|
335
|
+
return self[SOURCE][prop];
|
|
336
|
+
}
|
|
337
|
+
return Reflect.get(target, prop, receiver);
|
|
338
|
+
},
|
|
339
|
+
set(target, prop, value, receiver) {
|
|
340
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
341
|
+
if (!test) {
|
|
342
|
+
throw new Error(`Cannot set read-only property '${String(prop)}' on ManagedObject`);
|
|
343
|
+
}
|
|
344
|
+
})(editable) : {};
|
|
345
|
+
const reflect = Reflect.set(target, prop, value, receiver);
|
|
346
|
+
if (!reflect) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
if (!field.type) {
|
|
350
|
+
cache.setAttr(identifier, path, self[SOURCE]);
|
|
351
|
+
} else {
|
|
352
|
+
const transform = schema.transformation(field);
|
|
353
|
+
const val = transform.serialize(self[SOURCE], field.options ?? null, owner);
|
|
354
|
+
cache.setAttr(identifier, path, val);
|
|
355
|
+
}
|
|
356
|
+
_SIGNAL.shouldReset = true;
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
return proxy;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const ManagedArrayMap = getOrSetGlobal('ManagedArrayMap', new Map());
|
|
364
|
+
const ManagedObjectMap = getOrSetGlobal('ManagedObjectMap', new Map());
|
|
365
|
+
function computeLocal(record, field, prop) {
|
|
366
|
+
let signal = peekSignal(record, prop);
|
|
367
|
+
if (!signal) {
|
|
368
|
+
signal = getSignal(record, prop, false);
|
|
369
|
+
signal.lastValue = field.options?.defaultValue ?? null;
|
|
370
|
+
}
|
|
371
|
+
return signal.lastValue;
|
|
372
|
+
}
|
|
373
|
+
function peekManagedArray(record, field) {
|
|
374
|
+
const managedArrayMapForRecord = ManagedArrayMap.get(record);
|
|
375
|
+
if (managedArrayMapForRecord) {
|
|
376
|
+
return managedArrayMapForRecord.get(field);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function peekManagedObject(record, field) {
|
|
380
|
+
const managedObjectMapForRecord = ManagedObjectMap.get(record);
|
|
381
|
+
if (managedObjectMapForRecord) {
|
|
382
|
+
return managedObjectMapForRecord.get(field);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function computeField(schema, cache, record, identifier, field, prop) {
|
|
386
|
+
const rawValue = cache.getAttr(identifier, prop);
|
|
387
|
+
if (!field.type) {
|
|
388
|
+
return rawValue;
|
|
389
|
+
}
|
|
390
|
+
const transform = schema.transformation(field);
|
|
391
|
+
return transform.hydrate(rawValue, field.options ?? null, record);
|
|
392
|
+
}
|
|
393
|
+
function computeArray(store, schema, cache, record, identifier, field, path, isSchemaArray, editable, legacy) {
|
|
394
|
+
// the thing we hand out needs to know its owner and path in a private manner
|
|
395
|
+
// its "address" is the parent identifier (identifier) + field name (field.name)
|
|
396
|
+
// in the nested object case field name here is the full dot path from root resource to this value
|
|
397
|
+
// its "key" is the field on the parent record
|
|
398
|
+
// its "owner" is the parent record
|
|
399
|
+
|
|
400
|
+
const managedArrayMapForRecord = ManagedArrayMap.get(record);
|
|
401
|
+
let managedArray;
|
|
402
|
+
if (managedArrayMapForRecord) {
|
|
403
|
+
managedArray = managedArrayMapForRecord.get(field);
|
|
404
|
+
}
|
|
405
|
+
if (managedArray) {
|
|
406
|
+
return managedArray;
|
|
407
|
+
} else {
|
|
408
|
+
const rawValue = cache.getAttr(identifier, path);
|
|
409
|
+
if (!rawValue) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
managedArray = new ManagedArray(store, schema, cache, field, rawValue, identifier, path, record, isSchemaArray, editable, legacy);
|
|
413
|
+
if (!managedArrayMapForRecord) {
|
|
414
|
+
ManagedArrayMap.set(record, new Map([[field, managedArray]]));
|
|
415
|
+
} else {
|
|
416
|
+
managedArrayMapForRecord.set(field, managedArray);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return managedArray;
|
|
420
|
+
}
|
|
421
|
+
function computeObject(schema, cache, record, identifier, field, path, editable, legacy) {
|
|
422
|
+
const managedObjectMapForRecord = ManagedObjectMap.get(record);
|
|
423
|
+
let managedObject;
|
|
424
|
+
if (managedObjectMapForRecord) {
|
|
425
|
+
managedObject = managedObjectMapForRecord.get(field);
|
|
426
|
+
}
|
|
427
|
+
if (managedObject) {
|
|
428
|
+
return managedObject;
|
|
429
|
+
} else {
|
|
430
|
+
let rawValue = cache.getAttr(identifier, path);
|
|
431
|
+
if (!rawValue) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
if (field.type) {
|
|
435
|
+
const transform = schema.transformation(field);
|
|
436
|
+
rawValue = transform.hydrate(rawValue, field.options ?? null, record);
|
|
437
|
+
}
|
|
438
|
+
managedObject = new ManagedObject(schema, cache, field, rawValue, identifier, path, record, editable, legacy);
|
|
439
|
+
if (!managedObjectMapForRecord) {
|
|
440
|
+
ManagedObjectMap.set(record, new Map([[field, managedObject]]));
|
|
441
|
+
} else {
|
|
442
|
+
managedObjectMapForRecord.set(field, managedObject);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return managedObject;
|
|
446
|
+
}
|
|
447
|
+
function computeSchemaObject(store, cache, record, identifier, field, path, legacy, editable) {
|
|
448
|
+
const schemaObjectMapForRecord = ManagedObjectMap.get(record);
|
|
449
|
+
let schemaObject;
|
|
450
|
+
if (schemaObjectMapForRecord) {
|
|
451
|
+
schemaObject = schemaObjectMapForRecord.get(field);
|
|
452
|
+
}
|
|
453
|
+
if (schemaObject) {
|
|
454
|
+
return schemaObject;
|
|
455
|
+
} else {
|
|
456
|
+
const rawValue = cache.getAttr(identifier, path);
|
|
457
|
+
if (!rawValue) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const embeddedPath = path.slice();
|
|
461
|
+
schemaObject = new SchemaRecord(store, identifier, {
|
|
462
|
+
[Editable]: editable,
|
|
463
|
+
[Legacy]: legacy
|
|
464
|
+
}, true, field.type, embeddedPath);
|
|
465
|
+
}
|
|
466
|
+
if (!schemaObjectMapForRecord) {
|
|
467
|
+
ManagedObjectMap.set(record, new Map([[field, schemaObject]]));
|
|
468
|
+
} else {
|
|
469
|
+
schemaObjectMapForRecord.set(field, schemaObject);
|
|
470
|
+
}
|
|
471
|
+
return schemaObject;
|
|
472
|
+
}
|
|
473
|
+
function computeAttribute(cache, identifier, prop) {
|
|
474
|
+
return cache.getAttr(identifier, prop);
|
|
475
|
+
}
|
|
476
|
+
function computeDerivation(schema, record, identifier, field, prop) {
|
|
477
|
+
return schema.derivation(field)(record, field.options ?? null, prop);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// TODO probably this should just be a Document
|
|
481
|
+
// but its separate until we work out the lid situation
|
|
482
|
+
class ResourceRelationship {
|
|
483
|
+
constructor(store, cache, parent, identifier, field, name) {
|
|
484
|
+
const rawValue = cache.getRelationship(identifier, name);
|
|
485
|
+
|
|
486
|
+
// TODO setup true lids for relationship documents
|
|
487
|
+
// @ts-expect-error we need to give relationship documents a lid
|
|
488
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
489
|
+
this.lid = rawValue.lid ?? rawValue.links?.self ?? `relationship:${identifier.lid}.${name}`;
|
|
490
|
+
this.data = rawValue.data ? store.peekRecord(rawValue.data) : null;
|
|
491
|
+
this.name = name;
|
|
492
|
+
if (macroCondition(getGlobalConfig().WarpDrive.env.DEBUG)) {
|
|
493
|
+
this.links = Object.freeze(Object.assign({}, rawValue.links));
|
|
494
|
+
this.meta = Object.freeze(Object.assign({}, rawValue.meta));
|
|
495
|
+
} else {
|
|
496
|
+
this.links = rawValue.links ?? {};
|
|
497
|
+
this.meta = rawValue.meta ?? {};
|
|
498
|
+
}
|
|
499
|
+
this[RecordStore] = store;
|
|
500
|
+
this[Parent] = parent;
|
|
501
|
+
}
|
|
502
|
+
fetch(options) {
|
|
503
|
+
const url = options?.url ?? getHref(this.links.related) ?? getHref(this.links.self) ?? null;
|
|
504
|
+
if (!url) {
|
|
505
|
+
throw new Error(`Cannot ${options?.method ?? 'fetch'} ${this[Parent][Identifier].type}.${String(this.name)} because it has no related link`);
|
|
506
|
+
}
|
|
507
|
+
const request = Object.assign({
|
|
508
|
+
url,
|
|
509
|
+
method: 'GET'
|
|
510
|
+
}, options);
|
|
511
|
+
return this[RecordStore].request(request);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
defineSignal(ResourceRelationship.prototype, 'data');
|
|
515
|
+
defineSignal(ResourceRelationship.prototype, 'links');
|
|
516
|
+
defineSignal(ResourceRelationship.prototype, 'meta');
|
|
517
|
+
function getHref(link) {
|
|
518
|
+
if (!link) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
if (typeof link === 'string') {
|
|
522
|
+
return link;
|
|
523
|
+
}
|
|
524
|
+
return link.href;
|
|
525
|
+
}
|
|
526
|
+
function computeResource(store, cache, parent, identifier, field, prop) {
|
|
527
|
+
if (field.kind !== 'resource') {
|
|
528
|
+
throw new Error(`The schema for ${identifier.type}.${String(prop)} is not a resource relationship`);
|
|
529
|
+
}
|
|
530
|
+
return new ResourceRelationship(store, cache, parent, identifier, field, prop);
|
|
531
|
+
}
|
|
532
|
+
const HAS_MODEL_PACKAGE = dependencySatisfies('@ember-data/model', '*');
|
|
533
|
+
const getLegacySupport = macroCondition(dependencySatisfies('@ember-data/model', '*')) ? importSync('@ember-data/model/-private').lookupLegacySupport : null;
|
|
534
|
+
const IgnoredGlobalFields = new Set(['length', 'nodeType', 'then', 'setInterval', 'document', STRUCTURED]);
|
|
535
|
+
const symbolList = [Destroy, RecordStore, Identifier, Editable, Parent, Checkout, Legacy, Signals, EmbeddedPath, EmbeddedType];
|
|
536
|
+
const RecordSymbols = new Set(symbolList);
|
|
537
|
+
function isPathMatch(a, b) {
|
|
538
|
+
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
539
|
+
}
|
|
540
|
+
const Editables = new WeakMap();
|
|
541
|
+
class SchemaRecord {
|
|
542
|
+
constructor(store, identifier, Mode, isEmbedded = false, embeddedType = null, embeddedPath = null) {
|
|
543
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
544
|
+
const self = this;
|
|
545
|
+
this[RecordStore] = store;
|
|
546
|
+
if (isEmbedded) {
|
|
547
|
+
this[Parent] = identifier;
|
|
548
|
+
} else {
|
|
549
|
+
this[Identifier] = identifier;
|
|
550
|
+
}
|
|
551
|
+
const IS_EDITABLE = this[Editable] = Mode[Editable] ?? false;
|
|
552
|
+
this[Legacy] = Mode[Legacy] ?? false;
|
|
553
|
+
const schema = store.schema;
|
|
554
|
+
const cache = store.cache;
|
|
555
|
+
const identityField = schema.resource(identifier).identity;
|
|
556
|
+
this[EmbeddedType] = embeddedType;
|
|
557
|
+
this[EmbeddedPath] = embeddedPath;
|
|
558
|
+
let fields;
|
|
559
|
+
if (isEmbedded) {
|
|
560
|
+
fields = schema.fields({
|
|
561
|
+
type: embeddedType
|
|
562
|
+
});
|
|
563
|
+
} else {
|
|
564
|
+
fields = schema.fields(identifier);
|
|
565
|
+
}
|
|
566
|
+
const signals = new Map();
|
|
567
|
+
this[Signals] = signals;
|
|
568
|
+
const proxy = new Proxy(this, {
|
|
569
|
+
ownKeys() {
|
|
570
|
+
return Array.from(fields.keys());
|
|
571
|
+
},
|
|
572
|
+
has(target, prop) {
|
|
573
|
+
return fields.has(prop);
|
|
574
|
+
},
|
|
575
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
576
|
+
if (!fields.has(prop)) {
|
|
577
|
+
throw new Error(`No field named ${String(prop)} on ${identifier.type}`);
|
|
578
|
+
}
|
|
579
|
+
const schemaForField = fields.get(prop);
|
|
580
|
+
switch (schemaForField.kind) {
|
|
581
|
+
case 'derived':
|
|
582
|
+
return {
|
|
583
|
+
writable: false,
|
|
584
|
+
enumerable: true,
|
|
585
|
+
configurable: true
|
|
586
|
+
};
|
|
587
|
+
case '@local':
|
|
588
|
+
case 'field':
|
|
589
|
+
case 'attribute':
|
|
590
|
+
case 'resource':
|
|
591
|
+
case 'alias':
|
|
592
|
+
case 'belongsTo':
|
|
593
|
+
case 'hasMany':
|
|
594
|
+
case 'collection':
|
|
595
|
+
case 'schema-array':
|
|
596
|
+
case 'array':
|
|
597
|
+
case 'schema-object':
|
|
598
|
+
case 'object':
|
|
599
|
+
return {
|
|
600
|
+
writable: IS_EDITABLE,
|
|
601
|
+
enumerable: true,
|
|
602
|
+
configurable: true
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
get(target, prop, receiver) {
|
|
607
|
+
if (RecordSymbols.has(prop)) {
|
|
608
|
+
return target[prop];
|
|
609
|
+
}
|
|
610
|
+
if (prop === Symbol.toStringTag) {
|
|
611
|
+
return `SchemaRecord<${identifier.type}:${identifier.id} (${identifier.lid})>`;
|
|
612
|
+
}
|
|
613
|
+
if (prop === 'toString') {
|
|
614
|
+
return function () {
|
|
615
|
+
return `SchemaRecord<${identifier.type}:${identifier.id} (${identifier.lid})>`;
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
if (prop === 'toHTML') {
|
|
619
|
+
return function () {
|
|
620
|
+
return `<div>SchemaRecord<${identifier.type}:${identifier.id} (${identifier.lid})></div>`;
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
if (prop === Symbol.toPrimitive) {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// TODO make this a symbol
|
|
628
|
+
if (prop === '___notifications') {
|
|
629
|
+
return target.___notifications;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// SchemaRecord reserves use of keys that begin with these characters
|
|
633
|
+
// for its own usage.
|
|
634
|
+
// _, @, $, *
|
|
635
|
+
|
|
636
|
+
const maybeField = prop === identityField?.name ? identityField : fields.get(prop);
|
|
637
|
+
if (!maybeField) {
|
|
638
|
+
if (IgnoredGlobalFields.has(prop)) {
|
|
639
|
+
return undefined;
|
|
640
|
+
}
|
|
641
|
+
if (prop === 'constructor') {
|
|
642
|
+
return SchemaRecord;
|
|
643
|
+
}
|
|
644
|
+
// too many things check for random symbols
|
|
645
|
+
if (typeof prop === 'symbol') {
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
let type = identifier.type;
|
|
649
|
+
if (isEmbedded) {
|
|
650
|
+
type = embeddedType;
|
|
651
|
+
}
|
|
652
|
+
throw new Error(`No field named ${String(prop)} on ${type}`);
|
|
653
|
+
}
|
|
654
|
+
const field = maybeField.kind === 'alias' ? maybeField.options : maybeField;
|
|
655
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
656
|
+
if (!test) {
|
|
657
|
+
throw new Error(`Alias fields cannot alias '@id' '@local' '@hash' or 'derived' fields`);
|
|
658
|
+
}
|
|
659
|
+
})(maybeField.kind !== 'alias' || !['@id', '@local', '@hash', 'derived'].includes(maybeField.options.kind)) : {};
|
|
660
|
+
const propArray = isEmbedded ? embeddedPath.slice() : [];
|
|
661
|
+
// we use the field.name instead of prop here because we want to use the cache-path not
|
|
662
|
+
// the record path.
|
|
663
|
+
propArray.push(field.name);
|
|
664
|
+
// propArray.push(prop as string);
|
|
665
|
+
|
|
666
|
+
switch (field.kind) {
|
|
667
|
+
case '@id':
|
|
668
|
+
entangleSignal(signals, receiver, '@identity');
|
|
669
|
+
return identifier.id;
|
|
670
|
+
case '@hash':
|
|
671
|
+
// TODO pass actual cache value not {}
|
|
672
|
+
return schema.hashFn(field)({}, field.options ?? null, field.name ?? null);
|
|
673
|
+
case '@local':
|
|
674
|
+
{
|
|
675
|
+
const lastValue = computeLocal(receiver, field, prop);
|
|
676
|
+
entangleSignal(signals, receiver, prop);
|
|
677
|
+
return lastValue;
|
|
678
|
+
}
|
|
679
|
+
case 'field':
|
|
680
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
681
|
+
if (!test) {
|
|
682
|
+
throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
|
|
683
|
+
}
|
|
684
|
+
})(!target[Legacy]) : {};
|
|
685
|
+
entangleSignal(signals, receiver, field.name);
|
|
686
|
+
return computeField(schema, cache, target, identifier, field, propArray);
|
|
687
|
+
case 'attribute':
|
|
688
|
+
entangleSignal(signals, receiver, field.name);
|
|
689
|
+
return computeAttribute(cache, identifier, prop);
|
|
690
|
+
case 'resource':
|
|
691
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
692
|
+
if (!test) {
|
|
693
|
+
throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
|
|
694
|
+
}
|
|
695
|
+
})(!target[Legacy]) : {};
|
|
696
|
+
entangleSignal(signals, receiver, field.name);
|
|
697
|
+
return computeResource(store, cache, target, identifier, field, prop);
|
|
698
|
+
case 'derived':
|
|
699
|
+
return computeDerivation(schema, receiver, identifier, field, prop);
|
|
700
|
+
case 'schema-array':
|
|
701
|
+
entangleSignal(signals, receiver, field.name);
|
|
702
|
+
return computeArray(store, schema, cache, target, identifier, field, propArray, true, Mode[Editable], Mode[Legacy]);
|
|
703
|
+
case 'array':
|
|
704
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
705
|
+
if (!test) {
|
|
706
|
+
throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
|
|
707
|
+
}
|
|
708
|
+
})(!target[Legacy]) : {};
|
|
709
|
+
entangleSignal(signals, receiver, field.name);
|
|
710
|
+
return computeArray(store, schema, cache, target, identifier, field, propArray, false, Mode[Editable], Mode[Legacy]);
|
|
711
|
+
case 'object':
|
|
712
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
713
|
+
if (!test) {
|
|
714
|
+
throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
|
|
715
|
+
}
|
|
716
|
+
})(!target[Legacy]) : {};
|
|
717
|
+
entangleSignal(signals, receiver, field.name);
|
|
718
|
+
return computeObject(schema, cache, target, identifier, field, propArray, Mode[Editable], Mode[Legacy]);
|
|
719
|
+
case 'schema-object':
|
|
720
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
721
|
+
if (!test) {
|
|
722
|
+
throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
|
|
723
|
+
}
|
|
724
|
+
})(!target[Legacy]) : {};
|
|
725
|
+
entangleSignal(signals, receiver, field.name);
|
|
726
|
+
// run transform, then use that value as the object to manage
|
|
727
|
+
return computeSchemaObject(store, cache, target, identifier, field, propArray, Mode[Legacy], Mode[Editable]);
|
|
728
|
+
case 'belongsTo':
|
|
729
|
+
if (!HAS_MODEL_PACKAGE) {
|
|
730
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
731
|
+
{
|
|
732
|
+
throw new Error(`Cannot use belongsTo fields in your schema unless @ember-data/model is installed to provide legacy model support. ${field.name} should likely be migrated to be a resource field.`);
|
|
733
|
+
}
|
|
734
|
+
})() : {};
|
|
735
|
+
}
|
|
736
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
737
|
+
if (!test) {
|
|
738
|
+
throw new Error(`Expected to have a getLegacySupport function`);
|
|
739
|
+
}
|
|
740
|
+
})(getLegacySupport) : {};
|
|
741
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
742
|
+
if (!test) {
|
|
743
|
+
throw new Error(`Can only use belongsTo fields when the resource is in legacy mode`);
|
|
744
|
+
}
|
|
745
|
+
})(Mode[Legacy]) : {};
|
|
746
|
+
entangleSignal(signals, receiver, field.name);
|
|
747
|
+
return getLegacySupport(receiver).getBelongsTo(field.name);
|
|
748
|
+
case 'hasMany':
|
|
749
|
+
if (!HAS_MODEL_PACKAGE) {
|
|
750
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
751
|
+
{
|
|
752
|
+
throw new Error(`Cannot use hasMany fields in your schema unless @ember-data/model is installed to provide legacy model support. ${field.name} should likely be migrated to be a collection field.`);
|
|
753
|
+
}
|
|
754
|
+
})() : {};
|
|
755
|
+
}
|
|
756
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
757
|
+
if (!test) {
|
|
758
|
+
throw new Error(`Expected to have a getLegacySupport function`);
|
|
759
|
+
}
|
|
760
|
+
})(getLegacySupport) : {};
|
|
761
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
762
|
+
if (!test) {
|
|
763
|
+
throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
|
|
764
|
+
}
|
|
765
|
+
})(Mode[Legacy]) : {};
|
|
766
|
+
entangleSignal(signals, receiver, field.name);
|
|
767
|
+
return getLegacySupport(receiver).getHasMany(field.name);
|
|
768
|
+
default:
|
|
769
|
+
throw new Error(`Field '${String(prop)}' on '${identifier.type}' has the unknown kind '${field.kind}'`);
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
set(target, prop, value, receiver) {
|
|
773
|
+
if (!IS_EDITABLE) {
|
|
774
|
+
const type = isEmbedded ? embeddedType : identifier.type;
|
|
775
|
+
throw new Error(`Cannot set ${String(prop)} on ${type} because the record is not editable`);
|
|
776
|
+
}
|
|
777
|
+
const maybeField = prop === identityField?.name ? identityField : fields.get(prop);
|
|
778
|
+
if (!maybeField) {
|
|
779
|
+
const type = isEmbedded ? embeddedType : identifier.type;
|
|
780
|
+
throw new Error(`There is no field named ${String(prop)} on ${type}`);
|
|
781
|
+
}
|
|
782
|
+
const field = maybeField.kind === 'alias' ? maybeField.options : maybeField;
|
|
783
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
784
|
+
if (!test) {
|
|
785
|
+
throw new Error(`Alias fields cannot alias '@id' '@local' '@hash' or 'derived' fields`);
|
|
786
|
+
}
|
|
787
|
+
})(maybeField.kind !== 'alias' || !['@id', '@local', '@hash', 'derived'].includes(maybeField.options.kind)) : {};
|
|
788
|
+
const propArray = isEmbedded ? embeddedPath.slice() : [];
|
|
789
|
+
// we use the field.name instead of prop here because we want to use the cache-path not
|
|
790
|
+
// the record path.
|
|
791
|
+
propArray.push(field.name);
|
|
792
|
+
// propArray.push(prop as string);
|
|
793
|
+
|
|
794
|
+
switch (field.kind) {
|
|
795
|
+
case '@id':
|
|
796
|
+
{
|
|
797
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
798
|
+
if (!test) {
|
|
799
|
+
throw new Error(`Expected to receive a string id`);
|
|
800
|
+
}
|
|
801
|
+
})(typeof value === 'string' && value.length) : {};
|
|
802
|
+
const normalizedId = String(value);
|
|
803
|
+
const didChange = normalizedId !== identifier.id;
|
|
804
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
805
|
+
if (!test) {
|
|
806
|
+
throw new Error(`Cannot set ${identifier.type} record's id to ${normalizedId}, because id is already ${identifier.id}`);
|
|
807
|
+
}
|
|
808
|
+
})(!didChange || identifier.id === null) : {};
|
|
809
|
+
if (normalizedId !== null && didChange) {
|
|
810
|
+
store._instanceCache.setRecordId(identifier, normalizedId);
|
|
811
|
+
store.notifications.notify(identifier, 'identity');
|
|
812
|
+
}
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
case '@local':
|
|
816
|
+
{
|
|
817
|
+
const signal = getSignal(receiver, prop, true);
|
|
818
|
+
if (signal.lastValue !== value) {
|
|
819
|
+
signal.lastValue = value;
|
|
820
|
+
addToTransaction(signal);
|
|
821
|
+
}
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
case 'field':
|
|
825
|
+
{
|
|
826
|
+
if (!field.type) {
|
|
827
|
+
cache.setAttr(identifier, propArray, value);
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
const transform = schema.transformation(field);
|
|
831
|
+
const rawValue = transform.serialize(value, field.options ?? null, target);
|
|
832
|
+
cache.setAttr(identifier, propArray, rawValue);
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
case 'attribute':
|
|
836
|
+
{
|
|
837
|
+
cache.setAttr(identifier, propArray, value);
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
case 'array':
|
|
841
|
+
{
|
|
842
|
+
if (!field.type) {
|
|
843
|
+
cache.setAttr(identifier, propArray, value?.slice());
|
|
844
|
+
const peeked = peekManagedArray(self, field);
|
|
845
|
+
if (peeked) {
|
|
846
|
+
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
847
|
+
arrSignal.shouldReset = true;
|
|
848
|
+
}
|
|
849
|
+
if (!Array.isArray(value)) {
|
|
850
|
+
ManagedArrayMap.delete(target);
|
|
851
|
+
}
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
const transform = schema.transformation(field);
|
|
855
|
+
const rawValue = value.map(item => transform.serialize(item, field.options ?? null, target));
|
|
856
|
+
cache.setAttr(identifier, propArray, rawValue);
|
|
857
|
+
const peeked = peekManagedArray(self, field);
|
|
858
|
+
if (peeked) {
|
|
859
|
+
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
860
|
+
arrSignal.shouldReset = true;
|
|
861
|
+
}
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
case 'schema-array':
|
|
865
|
+
{
|
|
866
|
+
const arrayValue = value?.slice();
|
|
867
|
+
if (!Array.isArray(arrayValue)) {
|
|
868
|
+
ManagedArrayMap.delete(target);
|
|
869
|
+
}
|
|
870
|
+
cache.setAttr(identifier, propArray, arrayValue);
|
|
871
|
+
const peeked = peekManagedArray(self, field);
|
|
872
|
+
if (peeked) {
|
|
873
|
+
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
874
|
+
arrSignal.shouldReset = true;
|
|
875
|
+
}
|
|
876
|
+
if (!Array.isArray(value)) {
|
|
877
|
+
ManagedArrayMap.delete(target);
|
|
878
|
+
}
|
|
879
|
+
return true;
|
|
880
|
+
}
|
|
881
|
+
case 'object':
|
|
882
|
+
{
|
|
883
|
+
if (!field.type) {
|
|
884
|
+
let newValue = value;
|
|
885
|
+
if (value !== null) {
|
|
886
|
+
newValue = {
|
|
887
|
+
...value
|
|
888
|
+
};
|
|
889
|
+
} else {
|
|
890
|
+
ManagedObjectMap.delete(target);
|
|
891
|
+
}
|
|
892
|
+
cache.setAttr(identifier, propArray, newValue);
|
|
893
|
+
const peeked = peekManagedObject(self, field);
|
|
894
|
+
if (peeked) {
|
|
895
|
+
const objSignal = peeked[OBJECT_SIGNAL];
|
|
896
|
+
objSignal.shouldReset = true;
|
|
897
|
+
}
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
const transform = schema.transformation(field);
|
|
901
|
+
const rawValue = transform.serialize({
|
|
902
|
+
...value
|
|
903
|
+
}, field.options ?? null, target);
|
|
904
|
+
cache.setAttr(identifier, propArray, rawValue);
|
|
905
|
+
const peeked = peekManagedObject(self, field);
|
|
906
|
+
if (peeked) {
|
|
907
|
+
const objSignal = peeked[OBJECT_SIGNAL];
|
|
908
|
+
objSignal.shouldReset = true;
|
|
909
|
+
}
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
case 'schema-object':
|
|
913
|
+
{
|
|
914
|
+
let newValue = value;
|
|
915
|
+
if (value !== null) {
|
|
916
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
917
|
+
if (!test) {
|
|
918
|
+
throw new Error(`Expected value to be an object`);
|
|
919
|
+
}
|
|
920
|
+
})(typeof value === 'object') : {};
|
|
921
|
+
newValue = {
|
|
922
|
+
...value
|
|
923
|
+
};
|
|
924
|
+
const schemaFields = schema.fields({
|
|
925
|
+
type: field.type
|
|
926
|
+
});
|
|
927
|
+
for (const key of Object.keys(newValue)) {
|
|
928
|
+
if (!schemaFields.has(key)) {
|
|
929
|
+
throw new Error(`Field ${key} does not exist on schema object ${field.type}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
} else {
|
|
933
|
+
ManagedObjectMap.delete(target);
|
|
934
|
+
}
|
|
935
|
+
cache.setAttr(identifier, propArray, newValue);
|
|
936
|
+
// const peeked = peekManagedObject(self, field);
|
|
937
|
+
// if (peeked) {
|
|
938
|
+
// const objSignal = peeked[OBJECT_SIGNAL];
|
|
939
|
+
// objSignal.shouldReset = true;
|
|
940
|
+
// }
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
case 'derived':
|
|
944
|
+
{
|
|
945
|
+
throw new Error(`Cannot set ${String(prop)} on ${identifier.type} because it is derived`);
|
|
946
|
+
}
|
|
947
|
+
case 'belongsTo':
|
|
948
|
+
if (!HAS_MODEL_PACKAGE) {
|
|
949
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
950
|
+
{
|
|
951
|
+
throw new Error(`Cannot use belongsTo fields in your schema unless @ember-data/model is installed to provide legacy model support. ${field.name} should likely be migrated to be a resource field.`);
|
|
952
|
+
}
|
|
953
|
+
})() : {};
|
|
954
|
+
}
|
|
955
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
956
|
+
if (!test) {
|
|
957
|
+
throw new Error(`Expected to have a getLegacySupport function`);
|
|
958
|
+
}
|
|
959
|
+
})(getLegacySupport) : {};
|
|
960
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
961
|
+
if (!test) {
|
|
962
|
+
throw new Error(`Can only use belongsTo fields when the resource is in legacy mode`);
|
|
963
|
+
}
|
|
964
|
+
})(Mode[Legacy]) : {};
|
|
965
|
+
store._join(() => {
|
|
966
|
+
getLegacySupport(receiver).setDirtyBelongsTo(field.name, value);
|
|
967
|
+
});
|
|
968
|
+
return true;
|
|
969
|
+
case 'hasMany':
|
|
970
|
+
if (!HAS_MODEL_PACKAGE) {
|
|
971
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
972
|
+
{
|
|
973
|
+
throw new Error(`Cannot use hasMany fields in your schema unless @ember-data/model is installed to provide legacy model support. ${field.name} should likely be migrated to be a collection field.`);
|
|
974
|
+
}
|
|
975
|
+
})() : {};
|
|
976
|
+
}
|
|
977
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
978
|
+
if (!test) {
|
|
979
|
+
throw new Error(`Expected to have a getLegacySupport function`);
|
|
980
|
+
}
|
|
981
|
+
})(getLegacySupport) : {};
|
|
982
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
983
|
+
if (!test) {
|
|
984
|
+
throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
|
|
985
|
+
}
|
|
986
|
+
})(Mode[Legacy]) : {};
|
|
987
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
988
|
+
if (!test) {
|
|
989
|
+
throw new Error(`You must pass an array of records to set a hasMany relationship`);
|
|
990
|
+
}
|
|
991
|
+
})(Array.isArray(value)) : {};
|
|
992
|
+
store._join(() => {
|
|
993
|
+
const support = getLegacySupport(receiver);
|
|
994
|
+
const manyArray = support.getManyArray(field.name);
|
|
995
|
+
manyArray.splice(0, manyArray.length, ...value);
|
|
996
|
+
});
|
|
997
|
+
return true;
|
|
998
|
+
default:
|
|
999
|
+
throw new Error(`Unknown field kind ${field.kind}`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
// what signal do we need for embedded record?
|
|
1005
|
+
this.___notifications = store.notifications.subscribe(identifier, (_, type, key) => {
|
|
1006
|
+
switch (type) {
|
|
1007
|
+
case 'identity':
|
|
1008
|
+
{
|
|
1009
|
+
if (isEmbedded || !identityField) return; // base paths never apply to embedded records
|
|
1010
|
+
|
|
1011
|
+
if (identityField.name && identityField.kind === '@id') {
|
|
1012
|
+
const signal = signals.get('@identity');
|
|
1013
|
+
if (signal) {
|
|
1014
|
+
addToTransaction(signal);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
case 'attributes':
|
|
1020
|
+
if (key) {
|
|
1021
|
+
if (Array.isArray(key)) {
|
|
1022
|
+
if (!isEmbedded) return; // deep paths will be handled by embedded records
|
|
1023
|
+
// TODO we should have the notification manager
|
|
1024
|
+
// ensure it is safe for each callback to mutate this array
|
|
1025
|
+
if (isPathMatch(embeddedPath, key)) {
|
|
1026
|
+
// handle the notification
|
|
1027
|
+
// TODO we should likely handle this notification here
|
|
1028
|
+
// also we should add a LOGGING flag
|
|
1029
|
+
// eslint-disable-next-line no-console
|
|
1030
|
+
console.warn(`Notification unhandled for ${key.join(',')} on ${identifier.type}`, self);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// TODO we should add a LOGGING flag
|
|
1035
|
+
// console.log(`Deep notification skipped for ${key.join('.')} on ${identifier.type}`, self);
|
|
1036
|
+
// deep notify the key path
|
|
1037
|
+
} else {
|
|
1038
|
+
if (isEmbedded) return; // base paths never apply to embedded records
|
|
1039
|
+
|
|
1040
|
+
// TODO determine what LOGGING flag to wrap this in if any
|
|
1041
|
+
// console.log(`Notification for ${key} on ${identifier.type}`, self);
|
|
1042
|
+
const signal = signals.get(key);
|
|
1043
|
+
if (signal) {
|
|
1044
|
+
addToTransaction(signal);
|
|
1045
|
+
}
|
|
1046
|
+
const field = fields.get(key);
|
|
1047
|
+
if (field?.kind === 'array' || field?.kind === 'schema-array') {
|
|
1048
|
+
const peeked = peekManagedArray(self, field);
|
|
1049
|
+
if (peeked) {
|
|
1050
|
+
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
1051
|
+
arrSignal.shouldReset = true;
|
|
1052
|
+
addToTransaction(arrSignal);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (field?.kind === 'object') {
|
|
1056
|
+
const peeked = peekManagedObject(self, field);
|
|
1057
|
+
if (peeked) {
|
|
1058
|
+
const objSignal = peeked[OBJECT_SIGNAL];
|
|
1059
|
+
objSignal.shouldReset = true;
|
|
1060
|
+
addToTransaction(objSignal);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
break;
|
|
1066
|
+
case 'relationships':
|
|
1067
|
+
if (key) {
|
|
1068
|
+
if (Array.isArray(key)) ;else {
|
|
1069
|
+
if (isEmbedded) return; // base paths never apply to embedded records
|
|
1070
|
+
|
|
1071
|
+
const field = fields.get(key);
|
|
1072
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1073
|
+
if (!test) {
|
|
1074
|
+
throw new Error(`Expected relationshp ${key} to be the name of a field`);
|
|
1075
|
+
}
|
|
1076
|
+
})(field) : {};
|
|
1077
|
+
if (field.kind === 'belongsTo') {
|
|
1078
|
+
// TODO determine what LOGGING flag to wrap this in if any
|
|
1079
|
+
// console.log(`Notification for ${key} on ${identifier.type}`, self);
|
|
1080
|
+
const signal = signals.get(key);
|
|
1081
|
+
if (signal) {
|
|
1082
|
+
addToTransaction(signal);
|
|
1083
|
+
}
|
|
1084
|
+
// FIXME
|
|
1085
|
+
} else if (field.kind === 'resource') ;else if (field.kind === 'hasMany') {
|
|
1086
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1087
|
+
if (!test) {
|
|
1088
|
+
throw new Error(`Expected to have a getLegacySupport function`);
|
|
1089
|
+
}
|
|
1090
|
+
})(getLegacySupport) : {};
|
|
1091
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1092
|
+
if (!test) {
|
|
1093
|
+
throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
|
|
1094
|
+
}
|
|
1095
|
+
})(Mode[Legacy]) : {};
|
|
1096
|
+
const support = getLegacySupport(proxy);
|
|
1097
|
+
const manyArray = support && support._manyArrayCache[key];
|
|
1098
|
+
const hasPromise = support && support._relationshipPromisesCache[key];
|
|
1099
|
+
if (manyArray && hasPromise) {
|
|
1100
|
+
// do nothing, we will notify the ManyArray directly
|
|
1101
|
+
// once the fetch has completed.
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (manyArray) {
|
|
1105
|
+
manyArray.notify();
|
|
1106
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1107
|
+
if (!test) {
|
|
1108
|
+
throw new Error(`Expected options to exist on relationship meta`);
|
|
1109
|
+
}
|
|
1110
|
+
})(field.options) : {};
|
|
1111
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1112
|
+
if (!test) {
|
|
1113
|
+
throw new Error(`Expected async to exist on relationship meta options`);
|
|
1114
|
+
}
|
|
1115
|
+
})('async' in field.options) : {};
|
|
1116
|
+
if (field.options.async) {
|
|
1117
|
+
const signal = signals.get(key);
|
|
1118
|
+
if (signal) {
|
|
1119
|
+
addToTransaction(signal);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
} else if (field.kind === 'collection') ;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
return proxy;
|
|
1130
|
+
}
|
|
1131
|
+
[Destroy]() {
|
|
1132
|
+
if (this[Legacy]) {
|
|
1133
|
+
// @ts-expect-error
|
|
1134
|
+
this.isDestroying = true;
|
|
1135
|
+
// @ts-expect-error
|
|
1136
|
+
this.isDestroyed = true;
|
|
1137
|
+
}
|
|
1138
|
+
this[RecordStore].notifications.unsubscribe(this.___notifications);
|
|
1139
|
+
}
|
|
1140
|
+
[Checkout]() {
|
|
1141
|
+
const editable = Editables.get(this);
|
|
1142
|
+
if (editable) {
|
|
1143
|
+
return Promise.resolve(editable);
|
|
1144
|
+
}
|
|
1145
|
+
const embeddedType = this[EmbeddedType];
|
|
1146
|
+
const embeddedPath = this[EmbeddedPath];
|
|
1147
|
+
const isEmbedded = embeddedType !== null && embeddedPath !== null;
|
|
1148
|
+
if (isEmbedded) {
|
|
1149
|
+
throw new Error(`Cannot checkout an embedded record (yet)`);
|
|
1150
|
+
}
|
|
1151
|
+
const editableRecord = new SchemaRecord(this[RecordStore], this[Identifier], {
|
|
1152
|
+
[Editable]: true,
|
|
1153
|
+
[Legacy]: this[Legacy]
|
|
1154
|
+
}, isEmbedded, embeddedType, embeddedPath);
|
|
1155
|
+
setRecordIdentifier(editableRecord, recordIdentifierFor(this));
|
|
1156
|
+
return Promise.resolve(editableRecord);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
export { Checkout, Editable, Legacy, SchemaRecord };
|