@warp-drive/core 5.7.0-alpha.3 → 5.7.0-alpha.5
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/declarations/reactive/-private/default-mode.d.ts +73 -0
- package/declarations/reactive/-private/fields/get-field-key.d.ts +8 -0
- package/declarations/reactive/-private/fields/managed-array.d.ts +3 -5
- package/declarations/reactive/-private/fields/managed-object.d.ts +5 -3
- package/declarations/reactive/-private/kind/alias-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/array-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/attribute-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/belongs-to-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/collection-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/derived-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/generic-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/has-many-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/hash-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/identity-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/local-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/object-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/resource-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/schema-array-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/schema-object-field.d.ts +4 -0
- package/declarations/reactive/-private/record.d.ts +2 -4
- package/declarations/reactive/-private/schema.d.ts +6 -2
- package/declarations/reactive.d.ts +1 -0
- package/declarations/store/-types/q/schema-service.d.ts +20 -32
- package/declarations/types/schema/fields.d.ts +348 -11
- package/dist/graph/-private.js +1 -1
- package/dist/{handler-D2jjnIA-.js → handler-SdXlte1w.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/reactive.js +1156 -584
- package/dist/{request-state-CejVJgdj.js → request-state-CeN66aML.js} +12 -10
- package/dist/store/-private.js +2 -2
- package/dist/types/-private.js +1 -1
- package/dist/types/schema/fields.js +17 -2
- package/package.json +3 -3
- package/declarations/reactive/-private/fields/compute.d.ts +0 -43
package/dist/reactive.js
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import { isResourceSchema } from './types/schema/fields.js';
|
|
2
|
-
import { F as withSignalStore, T as isExtensionProp, U as performExtensionSet, H as consumeInternalSignal, V as performArrayExtensionGet, x as entangleSignal,
|
|
2
|
+
import { F as withSignalStore, T as isExtensionProp, U as performExtensionSet, H as consumeInternalSignal, V as performArrayExtensionGet, x as entangleSignal, E as peekInternalSignal, d as SOURCE$1, f as fastPush, l as RelatedCollection, J as getOrCreateInternalSignal, G as notifyInternalSignal, W as performObjectExtensionGet, y as defineSignal, B as Signals, h as setRecordIdentifier, r as recordIdentifierFor } from "./request-state-CeN66aML.js";
|
|
3
3
|
import { EnableHydration, STRUCTURED } from './types/request.js';
|
|
4
4
|
import { macroCondition, getGlobalConfig } from '@embroider/macros';
|
|
5
5
|
import { warn, deprecate } from '@ember/debug';
|
|
6
6
|
import './utils/string.js';
|
|
7
7
|
import { A as ARRAY_SIGNAL, O as OBJECT_SIGNAL, c as createMemo } from "./configure-B48bFHOl.js";
|
|
8
8
|
import { RecordStore, Type } from './types/symbols.js';
|
|
9
|
+
import { S as SOURCE, E as Editable, L as Legacy, D as Destroy, I as Identifier, P as Parent, a as EmbeddedPath, C as Checkout, b as EmbeddedField } from "./symbols-SIstXMLI.js";
|
|
9
10
|
import { getOrSetGlobal } from './types/-private.js';
|
|
10
|
-
import { S as SOURCE, E as Editable, L as Legacy, I as Identifier, P as Parent, a as EmbeddedPath, D as Destroy, C as Checkout, b as EmbeddedField } from "./symbols-SIstXMLI.js";
|
|
11
11
|
import './index.js';
|
|
12
|
+
function getAliasField(context) {
|
|
13
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
14
|
+
{
|
|
15
|
+
throw new Error(`Alias field access is not implemented`);
|
|
16
|
+
}
|
|
17
|
+
})() : {};
|
|
18
|
+
}
|
|
19
|
+
function setAliasField(context) {
|
|
20
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
21
|
+
{
|
|
22
|
+
throw new Error(`Alias field setting is not implemented`);
|
|
23
|
+
}
|
|
24
|
+
})() : {};
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
12
27
|
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']);
|
|
13
28
|
// const ARRAY_SETTER_METHODS = new Set<KeyType>(['push', 'pop', 'unshift', 'shift', 'splice', 'sort']);
|
|
14
29
|
const SYNC_PROPS = new Set(['[]', 'length']);
|
|
@@ -55,21 +70,27 @@ function safeForEach(instance, arr, store, callback, target) {
|
|
|
55
70
|
}
|
|
56
71
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
57
72
|
class ManagedArray {
|
|
58
|
-
constructor(
|
|
73
|
+
constructor(context, owner, data) {
|
|
59
74
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
60
75
|
const self = this;
|
|
61
76
|
this[SOURCE] = data?.slice();
|
|
62
|
-
const IS_EDITABLE = this[Editable] = editable ?? false;
|
|
63
|
-
this[Legacy] = legacy;
|
|
77
|
+
const IS_EDITABLE = this[Editable] = context.editable ?? false;
|
|
78
|
+
this[Legacy] = context.legacy;
|
|
79
|
+
const schema = context.store.schema;
|
|
80
|
+
const cache = context.store.cache;
|
|
81
|
+
const {
|
|
82
|
+
field
|
|
83
|
+
} = context;
|
|
64
84
|
const signals = withSignalStore(this);
|
|
65
85
|
let _SIGNAL = null;
|
|
66
86
|
const boundFns = new Map();
|
|
67
|
-
this.identifier =
|
|
68
|
-
this.path = path;
|
|
87
|
+
this.identifier = context.resourceKey;
|
|
88
|
+
this.path = context.path;
|
|
69
89
|
this.owner = owner;
|
|
70
90
|
let transaction = false;
|
|
71
|
-
const
|
|
72
|
-
|
|
91
|
+
const KeyMode = field.options?.key ?? '@identity';
|
|
92
|
+
// listener.
|
|
93
|
+
const RefStorage = KeyMode === '@identity' ? WeakMap :
|
|
73
94
|
// CAUTION CAUTION CAUTION
|
|
74
95
|
// this is a pile of lies
|
|
75
96
|
// the Map is Map<string, WeakRef<ReactiveResource>>
|
|
@@ -77,8 +98,8 @@ class ManagedArray {
|
|
|
77
98
|
// internal to a method like ours without us duplicating the code
|
|
78
99
|
// into two separate methods.
|
|
79
100
|
Map;
|
|
80
|
-
const ManagedRecordRefs =
|
|
81
|
-
const extensions = legacy ? schema.CAUTION_MEGA_DANGER_ZONE_arrayExtensions(field) : null;
|
|
101
|
+
const ManagedRecordRefs = field.kind === 'schema-array' ? new RefStorage() : null;
|
|
102
|
+
const extensions = context.legacy ? schema.CAUTION_MEGA_DANGER_ZONE_arrayExtensions(field) : null;
|
|
82
103
|
const proxy = new Proxy(this[SOURCE], {
|
|
83
104
|
get(target, prop, receiver) {
|
|
84
105
|
if (prop === ARRAY_SIGNAL) {
|
|
@@ -93,7 +114,7 @@ class ManagedArray {
|
|
|
93
114
|
const index = convertToInt(prop);
|
|
94
115
|
if (_SIGNAL.isStale && (index !== null || SYNC_PROPS.has(prop) || isArrayGetter(prop))) {
|
|
95
116
|
_SIGNAL.isStale = false;
|
|
96
|
-
const newData = cache.getAttr(
|
|
117
|
+
const newData = cache.getAttr(context.resourceKey, context.path);
|
|
97
118
|
if (newData && newData !== self[SOURCE]) {
|
|
98
119
|
self[SOURCE].length = 0;
|
|
99
120
|
self[SOURCE].push(...newData);
|
|
@@ -104,14 +125,72 @@ class ManagedArray {
|
|
|
104
125
|
}
|
|
105
126
|
if (prop === '[]') return consumeInternalSignal(_SIGNAL), receiver;
|
|
106
127
|
if (index !== null) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
128
|
+
if (!transaction) {
|
|
129
|
+
consumeInternalSignal(_SIGNAL);
|
|
130
|
+
}
|
|
131
|
+
const rawValue = target[index];
|
|
132
|
+
if (field.kind === 'array') {
|
|
133
|
+
if (field.type) {
|
|
134
|
+
const transform = schema.transformation(field);
|
|
135
|
+
return transform.hydrate(rawValue, field.options ?? null, self.owner);
|
|
136
|
+
}
|
|
137
|
+
return rawValue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* When the array is polymorphic, we need to determine the real type
|
|
142
|
+
* in order to apply the correct identity as schema-object identity
|
|
143
|
+
* is only required to be unique by type
|
|
144
|
+
*/
|
|
145
|
+
let objectType;
|
|
146
|
+
if (field.options?.polymorphic) {
|
|
147
|
+
const typePath = field.options.type ?? 'type';
|
|
148
|
+
// if we are polymorphic, then context.field.options.type will
|
|
149
|
+
// either specify a path on the rawValue to use as the type, defaulting to "type" or
|
|
150
|
+
// the special string "@hash" which tells us to treat field.type as a hashFn name with which
|
|
151
|
+
// to calc the type.
|
|
152
|
+
if (typePath === '@hash') {
|
|
153
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
154
|
+
if (!test) {
|
|
155
|
+
throw new Error(`Expected the field to define a hashFn as its type`);
|
|
156
|
+
}
|
|
157
|
+
})(field.type) : {};
|
|
158
|
+
const hashFn = schema.hashFn({
|
|
159
|
+
type: field.type
|
|
160
|
+
});
|
|
161
|
+
// TODO consider if there are better options and name args we could provide.
|
|
162
|
+
objectType = hashFn(rawValue, null, null);
|
|
163
|
+
} else {
|
|
164
|
+
objectType = rawValue[typePath];
|
|
165
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
166
|
+
if (!test) {
|
|
167
|
+
throw new Error(`Expected the type path for the field to be a value on the raw object`);
|
|
168
|
+
}
|
|
169
|
+
})(typePath && objectType && typeof objectType === 'string') : {};
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
173
|
+
if (!test) {
|
|
174
|
+
throw new Error(`A non-polymorphic SchemaArrayField must provide a SchemaObject type in its definition`);
|
|
175
|
+
}
|
|
176
|
+
})(field.type) : {};
|
|
177
|
+
objectType = field.type;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* When KeyMode=@hash the ReactiveResource is keyed into
|
|
182
|
+
* ManagedRecordRefs by the return value of @hash on the rawValue.
|
|
183
|
+
*
|
|
184
|
+
* This means that we could find a way to only recompute the identity
|
|
185
|
+
* when ARRAY_SIGNAL is dirty if hash performance becomes a bottleneck.
|
|
186
|
+
*/
|
|
187
|
+
let schemaObjectKeyValue;
|
|
188
|
+
if (KeyMode === '@hash') {
|
|
110
189
|
const hashField = schema.resource({
|
|
111
|
-
type:
|
|
190
|
+
type: objectType
|
|
112
191
|
}).identity;
|
|
113
192
|
const hashFn = schema.hashFn(hashField);
|
|
114
|
-
|
|
193
|
+
schemaObjectKeyValue = hashFn(rawValue, hashField.options ?? null, hashField.name);
|
|
115
194
|
} else {
|
|
116
195
|
// if mode is not @identity or @index, then access the key path.
|
|
117
196
|
// we should assert that `mode` is a string
|
|
@@ -119,69 +198,78 @@ class ManagedArray {
|
|
|
119
198
|
// and, we likely should lookup the associated field and throw an error IF
|
|
120
199
|
// the given field does not exist OR
|
|
121
200
|
// the field is anything other than a GenericField or LegacyAttributeField.
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
if (isSchemaArray) {
|
|
145
|
-
if (!transaction) {
|
|
146
|
-
consumeInternalSignal(_SIGNAL);
|
|
147
|
-
}
|
|
148
|
-
if (val) {
|
|
149
|
-
const recordRef = ManagedRecordRefs.get(val);
|
|
150
|
-
let record = recordRef?.deref();
|
|
151
|
-
if (!record) {
|
|
152
|
-
const recordPath = path.slice();
|
|
153
|
-
// this is a dirty lie since path is string[] but really we
|
|
154
|
-
// should change the types for paths to `Array<string | number>`
|
|
155
|
-
// TODO we should allow the schema for the field to define a "key"
|
|
156
|
-
// for stability. Default should be `@identity` which means that
|
|
157
|
-
// same object reference from cache should result in same ReactiveResource
|
|
158
|
-
// embedded object.
|
|
159
|
-
recordPath.push(index);
|
|
160
|
-
const recordIdentifier = self.owner[Identifier] || self.owner[Parent];
|
|
161
|
-
record = new ReactiveResource(store, recordIdentifier, {
|
|
162
|
-
[Editable]: self.owner[Editable],
|
|
163
|
-
[Legacy]: self.owner[Legacy]
|
|
164
|
-
}, true, field, recordPath);
|
|
165
|
-
// if mode is not @identity or @index, then access the key path now
|
|
166
|
-
// to determine the key value.
|
|
167
|
-
// chris says we can implement this as a special kind `@hash` which
|
|
168
|
-
// would be a function that only has access to the cache value and not
|
|
169
|
-
// the record itself, so derivation is possible but intentionally limited
|
|
170
|
-
// and non-reactive?
|
|
171
|
-
ManagedRecordRefs.set(val, new WeakRef(record));
|
|
201
|
+
if (macroCondition(getGlobalConfig().WarpDrive.env.DEBUG)) {
|
|
202
|
+
const isPathKeyMode = KeyMode !== '@identity' && KeyMode !== '@index';
|
|
203
|
+
if (isPathKeyMode) {
|
|
204
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
205
|
+
if (!test) {
|
|
206
|
+
throw new Error('mode must be a string');
|
|
207
|
+
}
|
|
208
|
+
})(typeof KeyMode === 'string' && KeyMode !== '') : {};
|
|
209
|
+
const modeField = schema.fields({
|
|
210
|
+
type: objectType
|
|
211
|
+
}).get(KeyMode);
|
|
212
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
213
|
+
if (!test) {
|
|
214
|
+
throw new Error('field must exist in schema');
|
|
215
|
+
}
|
|
216
|
+
})(modeField) : {};
|
|
217
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
218
|
+
if (!test) {
|
|
219
|
+
throw new Error('field must be a GenericField or LegacyAttributeField');
|
|
220
|
+
}
|
|
221
|
+
})(modeField.kind === 'field' || modeField.kind === 'attribute') : {};
|
|
172
222
|
}
|
|
173
|
-
return record;
|
|
174
223
|
}
|
|
175
|
-
|
|
224
|
+
schemaObjectKeyValue = KeyMode === '@identity' ? rawValue : KeyMode === '@index' ? index : rawValue[KeyMode];
|
|
176
225
|
}
|
|
177
|
-
if (!
|
|
178
|
-
|
|
226
|
+
if (!schemaObjectKeyValue) {
|
|
227
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
228
|
+
{
|
|
229
|
+
throw new Error(`Unexpected out of bounds access on SchemaArray`);
|
|
230
|
+
}
|
|
231
|
+
})() : {};
|
|
232
|
+
return undefined;
|
|
179
233
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
234
|
+
const recordRef = ManagedRecordRefs.get(schemaObjectKeyValue);
|
|
235
|
+
const record = recordRef?.value.deref();
|
|
236
|
+
|
|
237
|
+
// confirm the type and key still match
|
|
238
|
+
if (record && recordRef.type === objectType && recordRef.identity === schemaObjectKeyValue) {
|
|
239
|
+
if (recordRef.index !== index) {
|
|
240
|
+
recordRef.index = index;
|
|
241
|
+
recordRef.context.path[recordRef.context.path.length - 1] = index;
|
|
242
|
+
}
|
|
243
|
+
return record;
|
|
244
|
+
} else if (record) {
|
|
245
|
+
// TODO schedule idle once we can
|
|
246
|
+
void Promise.resolve().then(() => {
|
|
247
|
+
record[Destroy]();
|
|
248
|
+
});
|
|
183
249
|
}
|
|
184
|
-
|
|
250
|
+
const recordPath = context.path.slice();
|
|
251
|
+
// this is a dirty lie since path is string[] but really we
|
|
252
|
+
// should change the types for paths to `Array<string | number>`
|
|
253
|
+
recordPath.push(index);
|
|
254
|
+
const objectContext = {
|
|
255
|
+
store: context.store,
|
|
256
|
+
resourceKey: context.resourceKey,
|
|
257
|
+
modeName: context.modeName,
|
|
258
|
+
legacy: context.legacy,
|
|
259
|
+
editable: context.editable,
|
|
260
|
+
path: recordPath,
|
|
261
|
+
field: field,
|
|
262
|
+
value: objectType
|
|
263
|
+
};
|
|
264
|
+
const schemaObject = new ReactiveResource(objectContext);
|
|
265
|
+
ManagedRecordRefs.set(schemaObjectKeyValue, {
|
|
266
|
+
type: objectType,
|
|
267
|
+
identity: schemaObjectKeyValue,
|
|
268
|
+
index,
|
|
269
|
+
context: objectContext,
|
|
270
|
+
value: new WeakRef(schemaObject)
|
|
271
|
+
});
|
|
272
|
+
return schemaObject;
|
|
185
273
|
}
|
|
186
274
|
if (isArrayGetter(prop)) {
|
|
187
275
|
let fn = boundFns.get(prop);
|
|
@@ -190,7 +278,7 @@ class ManagedArray {
|
|
|
190
278
|
fn = function () {
|
|
191
279
|
consumeInternalSignal(_SIGNAL);
|
|
192
280
|
transaction = true;
|
|
193
|
-
const result = safeForEach(receiver, target, store, arguments[0], arguments[1]);
|
|
281
|
+
const result = safeForEach(receiver, target, context.store, arguments[0], arguments[1]);
|
|
194
282
|
transaction = false;
|
|
195
283
|
return result;
|
|
196
284
|
};
|
|
@@ -233,9 +321,9 @@ class ManagedArray {
|
|
|
233
321
|
},
|
|
234
322
|
set(target, prop, value, receiver) {
|
|
235
323
|
if (!IS_EDITABLE) {
|
|
236
|
-
let errorPath =
|
|
237
|
-
if (path) {
|
|
238
|
-
errorPath = path[path.length - 1];
|
|
324
|
+
let errorPath = context.resourceKey.type;
|
|
325
|
+
if (context.path) {
|
|
326
|
+
errorPath = context.path[context.path.length - 1];
|
|
239
327
|
}
|
|
240
328
|
throw new Error(`Cannot set ${String(prop)} on ${errorPath} because the record is not editable`);
|
|
241
329
|
}
|
|
@@ -253,19 +341,19 @@ class ManagedArray {
|
|
|
253
341
|
const reflect = Reflect.set(target, prop, value, receiver);
|
|
254
342
|
if (reflect) {
|
|
255
343
|
if (!field.type) {
|
|
256
|
-
cache.setAttr(
|
|
344
|
+
cache.setAttr(context.resourceKey, context.path, self[SOURCE]);
|
|
257
345
|
_SIGNAL.isStale = true;
|
|
258
346
|
return true;
|
|
259
347
|
}
|
|
260
348
|
let rawValue = self[SOURCE];
|
|
261
|
-
if (
|
|
349
|
+
if (field.kind !== 'schema-array') {
|
|
262
350
|
const transform = schema.transformation(field);
|
|
263
351
|
if (!transform) {
|
|
264
|
-
throw new Error(`No '${field.type}' transform defined for use by ${
|
|
352
|
+
throw new Error(`No '${field.type}' transform defined for use by ${context.resourceKey.type}.${String(prop)}`);
|
|
265
353
|
}
|
|
266
354
|
rawValue = self[SOURCE].map(item => transform.serialize(item, field.options ?? null, self.owner));
|
|
267
355
|
}
|
|
268
|
-
cache.setAttr(
|
|
356
|
+
cache.setAttr(context.resourceKey, context.path, rawValue);
|
|
269
357
|
_SIGNAL.isStale = true;
|
|
270
358
|
}
|
|
271
359
|
return reflect;
|
|
@@ -302,27 +390,453 @@ const desc = {
|
|
|
302
390
|
};
|
|
303
391
|
// compat(desc);
|
|
304
392
|
Object.defineProperty(ManagedArray.prototype, '[]', desc);
|
|
393
|
+
function getArrayField(context) {
|
|
394
|
+
const signal = entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
395
|
+
// the thing we hand out needs to know its owner and path in a private manner
|
|
396
|
+
// its "address" is the parent identifier (identifier) + field name (field.name)
|
|
397
|
+
// in the nested object case field name here is the full dot path from root resource to this value
|
|
398
|
+
// its "key" is the field on the parent record
|
|
399
|
+
// its "owner" is the parent record
|
|
400
|
+
const {
|
|
401
|
+
record
|
|
402
|
+
} = context;
|
|
403
|
+
let managedArray = signal.value;
|
|
404
|
+
if (managedArray) {
|
|
405
|
+
return managedArray;
|
|
406
|
+
} else {
|
|
407
|
+
const {
|
|
408
|
+
store,
|
|
409
|
+
resourceKey,
|
|
410
|
+
path
|
|
411
|
+
} = context;
|
|
412
|
+
const {
|
|
413
|
+
cache
|
|
414
|
+
} = store;
|
|
415
|
+
const rawValue = context.editable ? cache.getAttr(resourceKey, path) : cache.getRemoteAttr(resourceKey, path);
|
|
416
|
+
if (!rawValue) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
managedArray = new ManagedArray(context, record, rawValue);
|
|
420
|
+
signal.value = managedArray;
|
|
421
|
+
}
|
|
422
|
+
return managedArray;
|
|
423
|
+
}
|
|
424
|
+
function setArrayField(context) {
|
|
425
|
+
const {
|
|
426
|
+
field,
|
|
427
|
+
record,
|
|
428
|
+
value
|
|
429
|
+
} = context;
|
|
430
|
+
const {
|
|
431
|
+
cache,
|
|
432
|
+
schema
|
|
433
|
+
} = context.store;
|
|
434
|
+
const fieldSignal = peekInternalSignal(context.signals, context.path.at(-1));
|
|
435
|
+
const peeked = fieldSignal?.value;
|
|
436
|
+
const transform = field.type ? schema.transformation(field) : null;
|
|
437
|
+
const rawValue = field.type ? value.map(item => transform.serialize(item, field.options ?? null, record)) : value?.slice();
|
|
438
|
+
cache.setAttr(context.resourceKey, context.path, rawValue);
|
|
439
|
+
if (peeked) {
|
|
440
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
441
|
+
if (!test) {
|
|
442
|
+
throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
|
|
443
|
+
}
|
|
444
|
+
})(ARRAY_SIGNAL in peeked) : {};
|
|
445
|
+
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
446
|
+
arrSignal.isStale = true;
|
|
447
|
+
// TODO run array destroy?
|
|
448
|
+
}
|
|
449
|
+
if (!Array.isArray(rawValue) && fieldSignal) {
|
|
450
|
+
fieldSignal.value = null;
|
|
451
|
+
}
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
function getAttributeField(context) {
|
|
455
|
+
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
456
|
+
const {
|
|
457
|
+
cache
|
|
458
|
+
} = context.store;
|
|
459
|
+
return context.editable ? cache.getAttr(context.resourceKey, context.path) : cache.getRemoteAttr(context.resourceKey, context.path);
|
|
460
|
+
}
|
|
461
|
+
function setAttributeField(context) {
|
|
462
|
+
context.store.cache.setAttr(context.resourceKey, context.path, context.value);
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
const InvalidKinds = ['alias', 'derived', '@local'];
|
|
466
|
+
function isInvalidKind(kind) {
|
|
467
|
+
return InvalidKinds.includes(kind);
|
|
468
|
+
}
|
|
469
|
+
function isNonIdentityCacheableField(field) {
|
|
470
|
+
return !isInvalidKind(field.kind) && field.kind !== '@id' && field.kind !== '@hash';
|
|
471
|
+
}
|
|
472
|
+
function getFieldCacheKeyStrict(field) {
|
|
473
|
+
return field.sourceKey || field.name;
|
|
474
|
+
}
|
|
475
|
+
function getFieldCacheKey(field) {
|
|
476
|
+
return 'sourceKey' in field && field.sourceKey ? field.sourceKey : field.name;
|
|
477
|
+
}
|
|
478
|
+
function getBelongsToField(context) {
|
|
479
|
+
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
480
|
+
const {
|
|
481
|
+
field,
|
|
482
|
+
resourceKey,
|
|
483
|
+
store
|
|
484
|
+
} = context;
|
|
485
|
+
const {
|
|
486
|
+
schema,
|
|
487
|
+
cache
|
|
488
|
+
} = store;
|
|
489
|
+
if (field.options.linksMode) {
|
|
490
|
+
const rawValue = context.editable ? cache.getRelationship(resourceKey, getFieldCacheKeyStrict(field)) : cache.getRemoteRelationship(resourceKey, getFieldCacheKeyStrict(field));
|
|
491
|
+
return rawValue.data ? store.peekRecord(rawValue.data) : null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// FIXME move this to a "LegacyMode" make this part of "PolarisMode"
|
|
495
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
496
|
+
if (!test) {
|
|
497
|
+
throw new Error(`Can only use belongsTo fields when the resource is in legacy mode`);
|
|
498
|
+
}
|
|
499
|
+
})(context.legacy) : {};
|
|
500
|
+
return schema._kind('@legacy', 'belongsTo').get(store, context.record, resourceKey, field);
|
|
501
|
+
}
|
|
502
|
+
function setBelongsToField(context) {
|
|
503
|
+
const {
|
|
504
|
+
store
|
|
505
|
+
} = context;
|
|
506
|
+
const {
|
|
507
|
+
schema
|
|
508
|
+
} = store;
|
|
509
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
510
|
+
if (!test) {
|
|
511
|
+
throw new Error(`Can only mutate belongsTo fields when the resource is in legacy mode`);
|
|
512
|
+
}
|
|
513
|
+
})(context.legacy) : {};
|
|
514
|
+
schema._kind('@legacy', 'belongsTo').set(store, context.record, context.resourceKey, context.field, context.value);
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
function getCollectionField(context) {
|
|
518
|
+
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
519
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
520
|
+
{
|
|
521
|
+
throw new Error(`Accessing collection fields is not yet implemented`);
|
|
522
|
+
}
|
|
523
|
+
})() : {};
|
|
524
|
+
}
|
|
525
|
+
function setCollectionField(context) {
|
|
526
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
527
|
+
{
|
|
528
|
+
throw new Error(`Setting collection fields is not yet implemented`);
|
|
529
|
+
}
|
|
530
|
+
})() : {};
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
function getDerivedField(context) {
|
|
534
|
+
const {
|
|
535
|
+
schema
|
|
536
|
+
} = context.store;
|
|
537
|
+
return schema.derivation(context.field)(context.record, context.field.options ?? null, context.field.name);
|
|
538
|
+
}
|
|
539
|
+
function setDerivedField(context) {
|
|
540
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
541
|
+
{
|
|
542
|
+
throw new Error(`ILLEGAL SET: Cannot set '${context.path.join('.')}' on '${context.resourceKey.type}' as ${context.field.kind} fields are not mutable`);
|
|
543
|
+
}
|
|
544
|
+
})() : {};
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
function getGenericField(context) {
|
|
548
|
+
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
549
|
+
const {
|
|
550
|
+
cache,
|
|
551
|
+
schema
|
|
552
|
+
} = context.store;
|
|
553
|
+
const rawValue = context.editable ? cache.getAttr(context.resourceKey, context.path) : cache.getRemoteAttr(context.resourceKey, context.path);
|
|
554
|
+
const {
|
|
555
|
+
field
|
|
556
|
+
} = context;
|
|
557
|
+
if (!field.type) {
|
|
558
|
+
return rawValue;
|
|
559
|
+
}
|
|
560
|
+
const transform = schema.transformation(field);
|
|
561
|
+
return transform.hydrate(rawValue, field.options ?? null, context.record);
|
|
562
|
+
}
|
|
563
|
+
function setGenericField(context) {
|
|
564
|
+
const {
|
|
565
|
+
cache,
|
|
566
|
+
schema
|
|
567
|
+
} = context.store;
|
|
568
|
+
const {
|
|
569
|
+
field
|
|
570
|
+
} = context;
|
|
571
|
+
if (!field.type) {
|
|
572
|
+
cache.setAttr(context.resourceKey, context.path, context.value);
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
const transform = schema.transformation(field);
|
|
576
|
+
const rawValue = transform.serialize(context.value, field.options ?? null, context.record);
|
|
577
|
+
cache.setAttr(context.resourceKey, context.path, rawValue);
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
class ManyArrayManager {
|
|
581
|
+
constructor(record, editable) {
|
|
582
|
+
this.record = record;
|
|
583
|
+
this.store = record[RecordStore];
|
|
584
|
+
this.identifier = record[Identifier];
|
|
585
|
+
this.editable = editable;
|
|
586
|
+
}
|
|
587
|
+
_syncArray(array) {
|
|
588
|
+
const method = this.editable ? 'getRelationship' : 'getRemoteRelationship';
|
|
589
|
+
// FIXME field needs to use sourceKey
|
|
590
|
+
const rawValue = this.store.cache[method](this.identifier, array.key);
|
|
591
|
+
if (rawValue.meta) {
|
|
592
|
+
array.meta = rawValue.meta;
|
|
593
|
+
}
|
|
594
|
+
if (rawValue.links) {
|
|
595
|
+
array.links = rawValue.links;
|
|
596
|
+
}
|
|
597
|
+
const currentState = array[SOURCE$1];
|
|
598
|
+
|
|
599
|
+
// unlike in the normal RecordArray case, we don't need to divorce the reference
|
|
600
|
+
// because we don't need to worry about associate/disassociate since the graph
|
|
601
|
+
// takes care of that for us
|
|
602
|
+
if (currentState !== rawValue.data) {
|
|
603
|
+
currentState.length = 0;
|
|
604
|
+
fastPush(currentState, rawValue.data);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
reloadHasMany(key, options) {
|
|
608
|
+
// FIXME field needs to use sourceKey
|
|
609
|
+
const field = this.store.schema.fields(this.identifier).get(key);
|
|
610
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
611
|
+
if (!test) {
|
|
612
|
+
throw new Error(`Expected a hasMany field for ${key}`);
|
|
613
|
+
}
|
|
614
|
+
})(field?.kind === 'hasMany') : {};
|
|
615
|
+
const cacheOptions = options ? extractCacheOptions(options) : {
|
|
616
|
+
reload: true
|
|
617
|
+
};
|
|
618
|
+
cacheOptions.types = [field.type];
|
|
619
|
+
const rawValue = this.store.cache.getRelationship(this.identifier, key);
|
|
620
|
+
const req = {
|
|
621
|
+
url: getRelatedLink(rawValue),
|
|
622
|
+
op: 'findHasMany',
|
|
623
|
+
method: 'GET',
|
|
624
|
+
records: rawValue.data,
|
|
625
|
+
cacheOptions,
|
|
626
|
+
options: {
|
|
627
|
+
field,
|
|
628
|
+
identifier: this.identifier,
|
|
629
|
+
links: rawValue.links,
|
|
630
|
+
meta: rawValue.meta
|
|
631
|
+
},
|
|
632
|
+
[EnableHydration]: false
|
|
633
|
+
};
|
|
634
|
+
return this.store.request(req);
|
|
635
|
+
}
|
|
636
|
+
mutate(mutation) {
|
|
637
|
+
this.store.cache.mutate(mutation);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function getRelatedLink(resource) {
|
|
641
|
+
const related = resource.links?.related;
|
|
642
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
643
|
+
if (!test) {
|
|
644
|
+
throw new Error(`Expected a related link`);
|
|
645
|
+
}
|
|
646
|
+
})(related) : {};
|
|
647
|
+
return typeof related === 'object' ? related.href : related;
|
|
648
|
+
}
|
|
649
|
+
function extractCacheOptions(options) {
|
|
650
|
+
const cacheOptions = {};
|
|
651
|
+
if ('reload' in options) {
|
|
652
|
+
cacheOptions.reload = options.reload;
|
|
653
|
+
}
|
|
654
|
+
if ('backgroundReload' in options) {
|
|
655
|
+
cacheOptions.backgroundReload = options.backgroundReload;
|
|
656
|
+
}
|
|
657
|
+
return cacheOptions;
|
|
658
|
+
}
|
|
659
|
+
function getHasManyField(context) {
|
|
660
|
+
const signal = entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
661
|
+
const {
|
|
662
|
+
store,
|
|
663
|
+
field
|
|
664
|
+
} = context;
|
|
665
|
+
if (field.options.linksMode) {
|
|
666
|
+
const {
|
|
667
|
+
record
|
|
668
|
+
} = context;
|
|
669
|
+
// the thing we hand out needs to know its owner and path in a private manner
|
|
670
|
+
// its "address" is the parent identifier (identifier) + field name (field.name)
|
|
671
|
+
// in the nested object case field name here is the full dot path from root resource to this value
|
|
672
|
+
// its "key" is the field on the parent record
|
|
673
|
+
// its "owner" is the parent record
|
|
674
|
+
|
|
675
|
+
const cached = signal.value;
|
|
676
|
+
if (cached) {
|
|
677
|
+
return cached;
|
|
678
|
+
}
|
|
679
|
+
const {
|
|
680
|
+
editable,
|
|
681
|
+
resourceKey
|
|
682
|
+
} = context;
|
|
683
|
+
const {
|
|
684
|
+
cache
|
|
685
|
+
} = store;
|
|
686
|
+
const rawValue = cache.getRelationship(resourceKey, getFieldCacheKeyStrict(field));
|
|
687
|
+
if (!rawValue) {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
const managedArray = new RelatedCollection({
|
|
691
|
+
store,
|
|
692
|
+
type: field.type,
|
|
693
|
+
identifier: resourceKey,
|
|
694
|
+
cache,
|
|
695
|
+
field: context.legacy ? field : undefined,
|
|
696
|
+
// we divorce the reference here because ManyArray mutates the target directly
|
|
697
|
+
// before sending the mutation op to the cache. We may be able to avoid this in the future
|
|
698
|
+
identifiers: rawValue.data?.slice(),
|
|
699
|
+
key: field.name,
|
|
700
|
+
meta: rawValue.meta || null,
|
|
701
|
+
links: rawValue.links || null,
|
|
702
|
+
isPolymorphic: field.options.polymorphic ?? false,
|
|
703
|
+
isAsync: field.options.async ?? false,
|
|
704
|
+
// TODO: Grab the proper value
|
|
705
|
+
_inverseIsAsync: false,
|
|
706
|
+
// @ts-expect-error Typescript doesn't have a way for us to thread the generic backwards so it infers unknown instead of T
|
|
707
|
+
manager: new ManyArrayManager(record, editable),
|
|
708
|
+
isLoaded: true,
|
|
709
|
+
allowMutation: editable
|
|
710
|
+
});
|
|
711
|
+
signal.value = managedArray;
|
|
712
|
+
return managedArray;
|
|
713
|
+
}
|
|
714
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
715
|
+
if (!test) {
|
|
716
|
+
throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
|
|
717
|
+
}
|
|
718
|
+
})(context.legacy) : {};
|
|
719
|
+
return store.schema._kind('@legacy', 'hasMany').get(store, context.record, context.resourceKey, field);
|
|
720
|
+
}
|
|
721
|
+
function setHasManyField(context) {
|
|
722
|
+
const {
|
|
723
|
+
store
|
|
724
|
+
} = context;
|
|
725
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
726
|
+
if (!test) {
|
|
727
|
+
throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
|
|
728
|
+
}
|
|
729
|
+
})(context.legacy) : {};
|
|
730
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
731
|
+
if (!test) {
|
|
732
|
+
throw new Error(`You must pass an array of records to set a hasMany relationship`);
|
|
733
|
+
}
|
|
734
|
+
})(Array.isArray(context.value)) : {};
|
|
735
|
+
store.schema._kind('@legacy', 'hasMany').set(store, context.record, context.resourceKey, context.field, context.value);
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
function getHashField(context) {
|
|
739
|
+
const {
|
|
740
|
+
field,
|
|
741
|
+
path,
|
|
742
|
+
resourceKey
|
|
743
|
+
} = context;
|
|
744
|
+
const {
|
|
745
|
+
schema,
|
|
746
|
+
cache
|
|
747
|
+
} = context.store;
|
|
748
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
749
|
+
if (!test) {
|
|
750
|
+
throw new Error(`Cannot use a ${field.kind} directly on a resource.`);
|
|
751
|
+
}
|
|
752
|
+
})(Array.isArray(path) && path.length > 1) : {};
|
|
753
|
+
const realPath = path.slice(0, -1);
|
|
754
|
+
const rawData = context.editable ? cache.getAttr(resourceKey, realPath) : cache.getRemoteAttr(resourceKey, realPath);
|
|
755
|
+
return schema.hashFn(field)(rawData, field.options ?? null, field.name ?? null);
|
|
756
|
+
}
|
|
757
|
+
function setHashField(context) {
|
|
758
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
759
|
+
{
|
|
760
|
+
throw new Error(`ILLEGAL SET: Cannot set '${Array.isArray(context.path) ? context.path.join('.') : context.path}' on '${context.resourceKey.type}' as ${context.field.kind} fields are not mutable`);
|
|
761
|
+
}
|
|
762
|
+
})() : {};
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
function getIdentityField(context) {
|
|
766
|
+
entangleSignal(context.signals, context.record, '@identity', null);
|
|
767
|
+
return context.resourceKey.id;
|
|
768
|
+
}
|
|
769
|
+
function setIdentityField(context) {
|
|
770
|
+
const {
|
|
771
|
+
value,
|
|
772
|
+
resourceKey,
|
|
773
|
+
store
|
|
774
|
+
} = context;
|
|
775
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
776
|
+
if (!test) {
|
|
777
|
+
throw new Error(`Expected to receive a string id`);
|
|
778
|
+
}
|
|
779
|
+
})(typeof value === 'string' && value.length) : {};
|
|
780
|
+
const normalizedId = String(value);
|
|
781
|
+
const didChange = normalizedId !== resourceKey.id;
|
|
782
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
783
|
+
if (!test) {
|
|
784
|
+
throw new Error(`Cannot set ${resourceKey.type} record's id to ${normalizedId}, because id is already ${resourceKey.id}`);
|
|
785
|
+
}
|
|
786
|
+
})(!didChange || resourceKey.id === null) : {};
|
|
787
|
+
if (normalizedId !== null && didChange) {
|
|
788
|
+
store._instanceCache.setRecordId(resourceKey, normalizedId);
|
|
789
|
+
store.notifications.notify(resourceKey, 'identity');
|
|
790
|
+
}
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
function getLocalField(context) {
|
|
794
|
+
const {
|
|
795
|
+
field
|
|
796
|
+
} = context;
|
|
797
|
+
const signal = getOrCreateInternalSignal(context.signals, context.record, field.name, field.options?.defaultValue ?? null);
|
|
798
|
+
consumeInternalSignal(signal);
|
|
799
|
+
return signal.value;
|
|
800
|
+
}
|
|
801
|
+
function setLocalField(context) {
|
|
802
|
+
const {
|
|
803
|
+
value
|
|
804
|
+
} = context;
|
|
805
|
+
const signal = getOrCreateInternalSignal(context.signals, context.record, context.field.name, value);
|
|
806
|
+
if (signal.value !== value) {
|
|
807
|
+
signal.value = value;
|
|
808
|
+
notifyInternalSignal(signal);
|
|
809
|
+
}
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
305
812
|
const ObjectSymbols = new Set([OBJECT_SIGNAL, Parent, SOURCE, Editable, EmbeddedPath]);
|
|
306
813
|
|
|
307
814
|
// const ignoredGlobalFields = new Set<string>(['setInterval', 'nodeType', 'nodeName', 'length', 'document', STRUCTURED]);
|
|
308
815
|
|
|
309
816
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
310
817
|
class ManagedObject {
|
|
311
|
-
constructor(
|
|
818
|
+
constructor(context) {
|
|
819
|
+
const {
|
|
820
|
+
field,
|
|
821
|
+
path
|
|
822
|
+
} = context;
|
|
312
823
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
313
824
|
const self = this;
|
|
314
|
-
this[SOURCE] = {
|
|
315
|
-
...data
|
|
316
|
-
};
|
|
825
|
+
this[SOURCE] = Object.assign({}, context.value);
|
|
317
826
|
const signals = withSignalStore(this);
|
|
318
827
|
const _SIGNAL = this[OBJECT_SIGNAL] = entangleSignal(signals, this, OBJECT_SIGNAL, undefined);
|
|
319
|
-
this[Editable] = editable;
|
|
320
|
-
this[Legacy] = legacy;
|
|
321
|
-
this[Parent] =
|
|
828
|
+
this[Editable] = context.editable;
|
|
829
|
+
this[Legacy] = context.legacy;
|
|
830
|
+
this[Parent] = context.resourceKey;
|
|
322
831
|
this[EmbeddedPath] = path;
|
|
832
|
+
const identifier = context.resourceKey;
|
|
833
|
+
const {
|
|
834
|
+
cache,
|
|
835
|
+
schema
|
|
836
|
+
} = context.store;
|
|
323
837
|
|
|
324
838
|
// prettier-ignore
|
|
325
|
-
const extensions = !legacy ? null : schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(field);
|
|
839
|
+
const extensions = !context.legacy ? null : schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(field, null);
|
|
326
840
|
const proxy = new Proxy(this[SOURCE], {
|
|
327
841
|
ownKeys() {
|
|
328
842
|
return Object.keys(self[SOURCE]);
|
|
@@ -332,7 +846,7 @@ class ManagedObject {
|
|
|
332
846
|
},
|
|
333
847
|
getOwnPropertyDescriptor(target, prop) {
|
|
334
848
|
return {
|
|
335
|
-
writable: editable,
|
|
849
|
+
writable: context.editable,
|
|
336
850
|
enumerable: true,
|
|
337
851
|
configurable: true
|
|
338
852
|
};
|
|
@@ -366,11 +880,9 @@ class ManagedObject {
|
|
|
366
880
|
if (newData && newData !== self[SOURCE]) {
|
|
367
881
|
if (field.type) {
|
|
368
882
|
const transform = schema.transformation(field);
|
|
369
|
-
newData = transform.hydrate(newData, field.options ?? null,
|
|
883
|
+
newData = transform.hydrate(newData, field.options ?? null, context.record);
|
|
370
884
|
}
|
|
371
|
-
self[SOURCE] = {
|
|
372
|
-
...newData
|
|
373
|
-
}; // Add type assertion for newData
|
|
885
|
+
self[SOURCE] = Object.assign({}, newData); // Add type assertion for newData
|
|
374
886
|
}
|
|
375
887
|
}
|
|
376
888
|
|
|
@@ -396,7 +908,7 @@ class ManagedObject {
|
|
|
396
908
|
if (!test) {
|
|
397
909
|
throw new Error(`Cannot set read-only property '${String(prop)}' on ManagedObject`);
|
|
398
910
|
}
|
|
399
|
-
})(editable) : {};
|
|
911
|
+
})(context.editable) : {};
|
|
400
912
|
|
|
401
913
|
// since objects function as dictionaries, we can't defer to schema/data before extensions
|
|
402
914
|
// unless the prop is in the existing data.
|
|
@@ -411,151 +923,29 @@ class ManagedObject {
|
|
|
411
923
|
cache.setAttr(identifier, path, self[SOURCE]);
|
|
412
924
|
} else {
|
|
413
925
|
const transform = schema.transformation(field);
|
|
414
|
-
const val = transform.serialize(self[SOURCE], field.options ?? null,
|
|
926
|
+
const val = transform.serialize(self[SOURCE], field.options ?? null, context.record);
|
|
415
927
|
cache.setAttr(identifier, path, val);
|
|
416
928
|
}
|
|
417
929
|
_SIGNAL.isStale = true;
|
|
418
930
|
return true;
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
return proxy;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
class ManyArrayManager {
|
|
425
|
-
constructor(record, editable) {
|
|
426
|
-
this.record = record;
|
|
427
|
-
this.store = record[RecordStore];
|
|
428
|
-
this.identifier = record[Identifier];
|
|
429
|
-
this.editable = editable;
|
|
430
|
-
}
|
|
431
|
-
_syncArray(array) {
|
|
432
|
-
const method = this.editable ? 'getRelationship' : 'getRemoteRelationship';
|
|
433
|
-
const rawValue = this.store.cache[method](this.identifier, array.key);
|
|
434
|
-
if (rawValue.meta) {
|
|
435
|
-
array.meta = rawValue.meta;
|
|
436
|
-
}
|
|
437
|
-
if (rawValue.links) {
|
|
438
|
-
array.links = rawValue.links;
|
|
439
|
-
}
|
|
440
|
-
const currentState = array[SOURCE$1];
|
|
441
|
-
|
|
442
|
-
// unlike in the normal RecordArray case, we don't need to divorce the reference
|
|
443
|
-
// because we don't need to worry about associate/disassociate since the graph
|
|
444
|
-
// takes care of that for us
|
|
445
|
-
if (currentState !== rawValue.data) {
|
|
446
|
-
currentState.length = 0;
|
|
447
|
-
fastPush(currentState, rawValue.data);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
reloadHasMany(key, options) {
|
|
451
|
-
const field = this.store.schema.fields(this.identifier).get(key);
|
|
452
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
453
|
-
if (!test) {
|
|
454
|
-
throw new Error(`Expected a hasMany field for ${key}`);
|
|
455
|
-
}
|
|
456
|
-
})(field?.kind === 'hasMany') : {};
|
|
457
|
-
const cacheOptions = options ? extractCacheOptions(options) : {
|
|
458
|
-
reload: true
|
|
459
|
-
};
|
|
460
|
-
cacheOptions.types = [field.type];
|
|
461
|
-
const rawValue = this.store.cache.getRelationship(this.identifier, key);
|
|
462
|
-
const req = {
|
|
463
|
-
url: getRelatedLink(rawValue),
|
|
464
|
-
op: 'findHasMany',
|
|
465
|
-
method: 'GET',
|
|
466
|
-
records: rawValue.data,
|
|
467
|
-
cacheOptions,
|
|
468
|
-
options: {
|
|
469
|
-
field,
|
|
470
|
-
identifier: this.identifier,
|
|
471
|
-
links: rawValue.links,
|
|
472
|
-
meta: rawValue.meta
|
|
473
|
-
},
|
|
474
|
-
[EnableHydration]: false
|
|
475
|
-
};
|
|
476
|
-
return this.store.request(req);
|
|
477
|
-
}
|
|
478
|
-
mutate(mutation) {
|
|
479
|
-
this.store.cache.mutate(mutation);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
function getRelatedLink(resource) {
|
|
483
|
-
const related = resource.links?.related;
|
|
484
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
485
|
-
if (!test) {
|
|
486
|
-
throw new Error(`Expected a related link`);
|
|
487
|
-
}
|
|
488
|
-
})(related) : {};
|
|
489
|
-
return typeof related === 'object' ? related.href : related;
|
|
490
|
-
}
|
|
491
|
-
function extractCacheOptions(options) {
|
|
492
|
-
const cacheOptions = {};
|
|
493
|
-
if ('reload' in options) {
|
|
494
|
-
cacheOptions.reload = options.reload;
|
|
495
|
-
}
|
|
496
|
-
if ('backgroundReload' in options) {
|
|
497
|
-
cacheOptions.backgroundReload = options.backgroundReload;
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
return proxy;
|
|
498
934
|
}
|
|
499
|
-
return cacheOptions;
|
|
500
935
|
}
|
|
501
|
-
const ManagedArrayMap = getOrSetGlobal('ManagedArrayMap', new Map());
|
|
502
936
|
const ManagedObjectMap = getOrSetGlobal('ManagedObjectMap', new Map());
|
|
503
|
-
function computeLocal(record, field, prop) {
|
|
504
|
-
const signals = withSignalStore(record);
|
|
505
|
-
const signal = getOrCreateInternalSignal(signals, record, prop, field.options?.defaultValue ?? null);
|
|
506
|
-
consumeInternalSignal(signal);
|
|
507
|
-
return signal.value;
|
|
508
|
-
}
|
|
509
|
-
function peekManagedArray(record, field) {
|
|
510
|
-
const managedArrayMapForRecord = ManagedArrayMap.get(record);
|
|
511
|
-
if (managedArrayMapForRecord) {
|
|
512
|
-
return managedArrayMapForRecord.get(field.name);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
937
|
function peekManagedObject(record, field) {
|
|
516
938
|
const managedObjectMapForRecord = ManagedObjectMap.get(record);
|
|
517
939
|
if (managedObjectMapForRecord) {
|
|
518
940
|
return managedObjectMapForRecord.get(field.name);
|
|
519
941
|
}
|
|
520
942
|
}
|
|
521
|
-
function
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return transform.hydrate(rawValue, field.options ?? null, record);
|
|
528
|
-
}
|
|
529
|
-
function computeArray(store, schema, cache, record, identifier, field, path, editable, legacy) {
|
|
530
|
-
const isSchemaArray = field.kind === 'schema-array';
|
|
531
|
-
// the thing we hand out needs to know its owner and path in a private manner
|
|
532
|
-
// its "address" is the parent identifier (identifier) + field name (field.name)
|
|
533
|
-
// in the nested object case field name here is the full dot path from root resource to this value
|
|
534
|
-
// its "key" is the field on the parent record
|
|
535
|
-
// its "owner" is the parent record
|
|
536
|
-
|
|
537
|
-
const managedArrayMapForRecord = ManagedArrayMap.get(record);
|
|
538
|
-
let managedArray;
|
|
539
|
-
if (managedArrayMapForRecord) {
|
|
540
|
-
managedArray = managedArrayMapForRecord.get(field.name);
|
|
541
|
-
}
|
|
542
|
-
if (managedArray) {
|
|
543
|
-
return managedArray;
|
|
544
|
-
} else {
|
|
545
|
-
const rawValue = editable ? cache.getAttr(identifier, path) : cache.getRemoteAttr(identifier, path);
|
|
546
|
-
if (!rawValue) {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
managedArray = new ManagedArray(store, schema, cache, field, rawValue, identifier, path, record, isSchemaArray, editable, legacy);
|
|
550
|
-
if (!managedArrayMapForRecord) {
|
|
551
|
-
ManagedArrayMap.set(record, new Map([[field.name, managedArray]]));
|
|
552
|
-
} else {
|
|
553
|
-
managedArrayMapForRecord.set(field.name, managedArray);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
return managedArray;
|
|
557
|
-
}
|
|
558
|
-
function computeObject(schema, cache, record, identifier, field, path, editable, legacy) {
|
|
943
|
+
function getObjectField(context) {
|
|
944
|
+
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
945
|
+
const {
|
|
946
|
+
record,
|
|
947
|
+
field
|
|
948
|
+
} = context;
|
|
559
949
|
const managedObjectMapForRecord = ManagedObjectMap.get(record);
|
|
560
950
|
let managedObject;
|
|
561
951
|
if (managedObjectMapForRecord) {
|
|
@@ -564,7 +954,16 @@ function computeObject(schema, cache, record, identifier, field, path, editable,
|
|
|
564
954
|
if (managedObject) {
|
|
565
955
|
return managedObject;
|
|
566
956
|
} else {
|
|
567
|
-
|
|
957
|
+
const {
|
|
958
|
+
store,
|
|
959
|
+
resourceKey,
|
|
960
|
+
path
|
|
961
|
+
} = context;
|
|
962
|
+
const {
|
|
963
|
+
cache,
|
|
964
|
+
schema
|
|
965
|
+
} = store;
|
|
966
|
+
let rawValue = context.editable ? cache.getAttr(resourceKey, path) : cache.getRemoteAttr(resourceKey, path);
|
|
568
967
|
if (!rawValue) {
|
|
569
968
|
return null;
|
|
570
969
|
}
|
|
@@ -572,7 +971,18 @@ function computeObject(schema, cache, record, identifier, field, path, editable,
|
|
|
572
971
|
const transform = schema.transformation(field);
|
|
573
972
|
rawValue = transform.hydrate(rawValue, field.options ?? null, record);
|
|
574
973
|
}
|
|
575
|
-
managedObject = new ManagedObject(
|
|
974
|
+
managedObject = new ManagedObject({
|
|
975
|
+
store,
|
|
976
|
+
resourceKey,
|
|
977
|
+
modeName: context.modeName,
|
|
978
|
+
legacy: context.legacy,
|
|
979
|
+
editable: context.editable,
|
|
980
|
+
path,
|
|
981
|
+
field,
|
|
982
|
+
record,
|
|
983
|
+
signals: context.signals,
|
|
984
|
+
value: rawValue
|
|
985
|
+
});
|
|
576
986
|
if (!managedObjectMapForRecord) {
|
|
577
987
|
ManagedObjectMap.set(record, new Map([[field.name, managedObject]]));
|
|
578
988
|
} else {
|
|
@@ -581,48 +991,64 @@ function computeObject(schema, cache, record, identifier, field, path, editable,
|
|
|
581
991
|
}
|
|
582
992
|
return managedObject;
|
|
583
993
|
}
|
|
584
|
-
function
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
994
|
+
function setObjectField(context) {
|
|
995
|
+
const {
|
|
996
|
+
field,
|
|
997
|
+
value,
|
|
998
|
+
record
|
|
999
|
+
} = context;
|
|
1000
|
+
const {
|
|
1001
|
+
cache,
|
|
1002
|
+
schema
|
|
1003
|
+
} = context.store;
|
|
1004
|
+
if (!field.type) {
|
|
1005
|
+
let newValue = value;
|
|
1006
|
+
if (value !== null) {
|
|
1007
|
+
newValue = {
|
|
1008
|
+
...value
|
|
1009
|
+
};
|
|
1010
|
+
} else {
|
|
1011
|
+
ManagedObjectMap.delete(record);
|
|
596
1012
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
[
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
ManagedObjectMap.set(record, new Map([[field.name, schemaObject]]));
|
|
605
|
-
} else {
|
|
606
|
-
schemaObjectMapForRecord.set(field.name, schemaObject);
|
|
1013
|
+
cache.setAttr(context.resourceKey, context.path, newValue);
|
|
1014
|
+
const peeked = peekManagedObject(record, field);
|
|
1015
|
+
if (peeked) {
|
|
1016
|
+
const objSignal = peeked[OBJECT_SIGNAL];
|
|
1017
|
+
objSignal.isStale = true;
|
|
1018
|
+
}
|
|
1019
|
+
return true;
|
|
607
1020
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1021
|
+
const transform = schema.transformation(field);
|
|
1022
|
+
const rawValue = transform.serialize({
|
|
1023
|
+
...value
|
|
1024
|
+
}, field.options ?? null, record);
|
|
1025
|
+
cache.setAttr(context.resourceKey, context.path, rawValue);
|
|
1026
|
+
const peeked = peekManagedObject(record, field);
|
|
1027
|
+
if (peeked) {
|
|
1028
|
+
const objSignal = peeked[OBJECT_SIGNAL];
|
|
1029
|
+
objSignal.isStale = true;
|
|
1030
|
+
}
|
|
1031
|
+
return true;
|
|
615
1032
|
}
|
|
1033
|
+
|
|
616
1034
|
// TODO probably this should just be a Document
|
|
617
1035
|
// but its separate until we work out the lid situation
|
|
618
1036
|
class ResourceRelationship {
|
|
619
|
-
constructor(
|
|
620
|
-
const
|
|
1037
|
+
constructor(context) {
|
|
1038
|
+
const {
|
|
1039
|
+
store,
|
|
1040
|
+
resourceKey
|
|
1041
|
+
} = context;
|
|
1042
|
+
const {
|
|
1043
|
+
cache
|
|
1044
|
+
} = store;
|
|
1045
|
+
const name = getFieldCacheKeyStrict(context.field);
|
|
1046
|
+
const rawValue = context.editable ? cache.getRelationship(resourceKey, name) : cache.getRemoteRelationship(resourceKey, name);
|
|
621
1047
|
|
|
622
1048
|
// TODO setup true lids for relationship documents
|
|
623
1049
|
// @ts-expect-error we need to give relationship documents a lid
|
|
624
1050
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
625
|
-
this.lid = rawValue.lid ?? rawValue.links?.self ?? `relationship:${
|
|
1051
|
+
this.lid = rawValue.lid ?? rawValue.links?.self ?? `relationship:${resourceKey.lid}.${name}`;
|
|
626
1052
|
this.data = rawValue.data ? store.peekRecord(rawValue.data) : null;
|
|
627
1053
|
this.name = name;
|
|
628
1054
|
if (macroCondition(getGlobalConfig().WarpDrive.env.DEBUG)) {
|
|
@@ -633,7 +1059,7 @@ class ResourceRelationship {
|
|
|
633
1059
|
this.meta = rawValue.meta ?? {};
|
|
634
1060
|
}
|
|
635
1061
|
this[RecordStore] = store;
|
|
636
|
-
this[Parent] =
|
|
1062
|
+
this[Parent] = context.record;
|
|
637
1063
|
}
|
|
638
1064
|
fetch(options) {
|
|
639
1065
|
const url = options?.url ?? getHref(this.links.related) ?? getHref(this.links.self) ?? null;
|
|
@@ -659,60 +1085,272 @@ function getHref(link) {
|
|
|
659
1085
|
}
|
|
660
1086
|
return link.href;
|
|
661
1087
|
}
|
|
662
|
-
function
|
|
663
|
-
|
|
664
|
-
|
|
1088
|
+
function getResourceField(context) {
|
|
1089
|
+
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
1090
|
+
return new ResourceRelationship(context);
|
|
1091
|
+
}
|
|
1092
|
+
function setResourceField(context) {
|
|
1093
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1094
|
+
{
|
|
1095
|
+
throw new Error(`setting resource relationships is not yet supported`);
|
|
1096
|
+
}
|
|
1097
|
+
})() : {};
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
function setSchemaArrayField(context) {
|
|
1101
|
+
const arrayValue = context.value?.slice();
|
|
1102
|
+
const fieldSignal = peekInternalSignal(context.signals, context.path.at(-1));
|
|
1103
|
+
const peeked = fieldSignal?.value;
|
|
1104
|
+
context.store.cache.setAttr(context.resourceKey, context.path, arrayValue);
|
|
1105
|
+
if (peeked) {
|
|
1106
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1107
|
+
if (!test) {
|
|
1108
|
+
throw new Error(`Expected the peekManagedArray for ${context.field.kind} to return a ManagedArray`);
|
|
1109
|
+
}
|
|
1110
|
+
})(ARRAY_SIGNAL in peeked) : {};
|
|
1111
|
+
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
1112
|
+
arrSignal.isStale = true;
|
|
1113
|
+
if (!Array.isArray(arrayValue)) {
|
|
1114
|
+
fieldSignal.value = null;
|
|
1115
|
+
}
|
|
665
1116
|
}
|
|
666
|
-
return
|
|
1117
|
+
return true;
|
|
667
1118
|
}
|
|
668
|
-
function
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1119
|
+
function getSchemaObjectField(context) {
|
|
1120
|
+
const signal = entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
1121
|
+
const {
|
|
1122
|
+
store,
|
|
1123
|
+
resourceKey,
|
|
1124
|
+
path
|
|
1125
|
+
} = context;
|
|
1126
|
+
const {
|
|
1127
|
+
cache
|
|
1128
|
+
} = store;
|
|
1129
|
+
const rawValue = context.editable ? cache.getAttr(resourceKey, path) : cache.getRemoteAttr(resourceKey, path);
|
|
1130
|
+
if (!rawValue) {
|
|
1131
|
+
if (signal.value) {
|
|
1132
|
+
const value = signal.value;
|
|
1133
|
+
// TODO if we had idle scheduling this should be done there.
|
|
1134
|
+
void Promise.resolve().then(() => {
|
|
1135
|
+
value.value[Destroy]();
|
|
1136
|
+
});
|
|
1137
|
+
signal.value = null;
|
|
1138
|
+
}
|
|
1139
|
+
return null;
|
|
679
1140
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1141
|
+
const {
|
|
1142
|
+
field
|
|
1143
|
+
} = context;
|
|
1144
|
+
const {
|
|
1145
|
+
schema
|
|
1146
|
+
} = store;
|
|
1147
|
+
let objectType;
|
|
1148
|
+
if (field.options?.polymorphic) {
|
|
1149
|
+
const typePath = field.options.type ?? 'type';
|
|
1150
|
+
// if we are polymorphic, then context.field.options.type will
|
|
1151
|
+
// either specify a path on the rawValue to use as the type, defaulting to "type" or
|
|
1152
|
+
// the special string "@hash" which tells us to treat field.type as a hashFn name with which
|
|
1153
|
+
// to calc the type.
|
|
1154
|
+
if (typePath === '@hash') {
|
|
1155
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1156
|
+
if (!test) {
|
|
1157
|
+
throw new Error(`Expected the field to define a hashFn as its type`);
|
|
1158
|
+
}
|
|
1159
|
+
})(field.type) : {};
|
|
1160
|
+
const hashFn = schema.hashFn({
|
|
1161
|
+
type: field.type
|
|
1162
|
+
});
|
|
1163
|
+
// TODO consider if there are better options and name args we could provide.
|
|
1164
|
+
objectType = hashFn(rawValue, null, null);
|
|
1165
|
+
} else {
|
|
1166
|
+
objectType = rawValue[typePath];
|
|
1167
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1168
|
+
if (!test) {
|
|
1169
|
+
throw new Error(`Expected the type path for the field to be a value on the raw object`);
|
|
1170
|
+
}
|
|
1171
|
+
})(typePath && objectType && typeof objectType === 'string') : {};
|
|
686
1172
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
// @ts-expect-error Typescript doesn't have a way for us to thread the generic backwards so it infers unknown instead of T
|
|
704
|
-
manager: new ManyArrayManager(record, editable),
|
|
705
|
-
isLoaded: true,
|
|
706
|
-
allowMutation: editable
|
|
707
|
-
});
|
|
708
|
-
if (!managedArrayMapForRecord) {
|
|
709
|
-
ManagedArrayMap.set(record, new Map([[field.name, managedArray]]));
|
|
1173
|
+
} else {
|
|
1174
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1175
|
+
if (!test) {
|
|
1176
|
+
throw new Error(`A non-polymorphic SchemaObjectField must provide a SchemaObject type in its definition`);
|
|
1177
|
+
}
|
|
1178
|
+
})(field.type) : {};
|
|
1179
|
+
objectType = field.type;
|
|
1180
|
+
}
|
|
1181
|
+
const hashField = schema.resource({
|
|
1182
|
+
type: objectType
|
|
1183
|
+
}).identity;
|
|
1184
|
+
const identity = hashField ? schema.hashFn(hashField)(rawValue, hashField.options ?? null, hashField.name) : field.name;
|
|
1185
|
+
const cachedSchemaObject = signal.value;
|
|
1186
|
+
if (cachedSchemaObject) {
|
|
1187
|
+
if (cachedSchemaObject.type === objectType && cachedSchemaObject.identity === identity) {
|
|
1188
|
+
return cachedSchemaObject.value;
|
|
710
1189
|
} else {
|
|
711
|
-
|
|
1190
|
+
// TODO if we had idle scheduling this should be done there.
|
|
1191
|
+
void Promise.resolve().then(() => {
|
|
1192
|
+
cachedSchemaObject.value[Destroy]();
|
|
1193
|
+
});
|
|
712
1194
|
}
|
|
713
1195
|
}
|
|
714
|
-
|
|
1196
|
+
const schemaObject = new ReactiveResource({
|
|
1197
|
+
store: context.store,
|
|
1198
|
+
resourceKey: context.resourceKey,
|
|
1199
|
+
modeName: context.modeName,
|
|
1200
|
+
legacy: context.legacy,
|
|
1201
|
+
editable: context.editable,
|
|
1202
|
+
path: context.path,
|
|
1203
|
+
field: context.field,
|
|
1204
|
+
value: objectType
|
|
1205
|
+
});
|
|
1206
|
+
signal.value = {
|
|
1207
|
+
type: objectType,
|
|
1208
|
+
identity: identity,
|
|
1209
|
+
value: schemaObject
|
|
1210
|
+
};
|
|
1211
|
+
return schemaObject;
|
|
1212
|
+
}
|
|
1213
|
+
function setSchemaObjectField(context) {
|
|
1214
|
+
const {
|
|
1215
|
+
store,
|
|
1216
|
+
value
|
|
1217
|
+
} = context;
|
|
1218
|
+
let newValue = value;
|
|
1219
|
+
if (value !== null) {
|
|
1220
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1221
|
+
if (!test) {
|
|
1222
|
+
throw new Error(`Expected value to be an object`);
|
|
1223
|
+
}
|
|
1224
|
+
})(typeof value === 'object') : {};
|
|
1225
|
+
newValue = {
|
|
1226
|
+
...value
|
|
1227
|
+
};
|
|
1228
|
+
const schemaFields = store.schema.fields({
|
|
1229
|
+
type: context.field.type
|
|
1230
|
+
});
|
|
1231
|
+
for (const key of Object.keys(newValue)) {
|
|
1232
|
+
if (!schemaFields.has(key)) {
|
|
1233
|
+
throw new Error(`Field ${key} does not exist on schema object ${context.field.type}`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
} else {
|
|
1237
|
+
ManagedObjectMap.delete(context.record);
|
|
1238
|
+
}
|
|
1239
|
+
store.cache.setAttr(context.resourceKey, context.path, newValue);
|
|
1240
|
+
// const peeked = peekManagedObject(self, field);
|
|
1241
|
+
// if (peeked) {
|
|
1242
|
+
// const objSignal = peeked[OBJECT_SIGNAL];
|
|
1243
|
+
// objSignal.isStale = true;
|
|
1244
|
+
// }
|
|
1245
|
+
return true;
|
|
715
1246
|
}
|
|
1247
|
+
const DefaultMode = {
|
|
1248
|
+
'@hash': {
|
|
1249
|
+
get: getHashField,
|
|
1250
|
+
set: setHashField,
|
|
1251
|
+
mutable: false,
|
|
1252
|
+
enumerable: false,
|
|
1253
|
+
serializable: false
|
|
1254
|
+
},
|
|
1255
|
+
'@id': {
|
|
1256
|
+
get: getIdentityField,
|
|
1257
|
+
set: setIdentityField,
|
|
1258
|
+
mutable: true,
|
|
1259
|
+
enumerable: true,
|
|
1260
|
+
serializable: true
|
|
1261
|
+
},
|
|
1262
|
+
'@local': {
|
|
1263
|
+
get: getLocalField,
|
|
1264
|
+
set: setLocalField,
|
|
1265
|
+
mutable: true,
|
|
1266
|
+
enumerable: false,
|
|
1267
|
+
serializable: false
|
|
1268
|
+
},
|
|
1269
|
+
alias: {
|
|
1270
|
+
get: getAliasField,
|
|
1271
|
+
set: setAliasField,
|
|
1272
|
+
mutable: true,
|
|
1273
|
+
enumerable: true,
|
|
1274
|
+
serializable: false
|
|
1275
|
+
},
|
|
1276
|
+
array: {
|
|
1277
|
+
get: getArrayField,
|
|
1278
|
+
set: setArrayField,
|
|
1279
|
+
mutable: true,
|
|
1280
|
+
enumerable: true,
|
|
1281
|
+
serializable: true
|
|
1282
|
+
},
|
|
1283
|
+
attribute: {
|
|
1284
|
+
get: getAttributeField,
|
|
1285
|
+
set: setAttributeField,
|
|
1286
|
+
mutable: true,
|
|
1287
|
+
enumerable: true,
|
|
1288
|
+
serializable: true
|
|
1289
|
+
},
|
|
1290
|
+
belongsTo: {
|
|
1291
|
+
get: getBelongsToField,
|
|
1292
|
+
set: setBelongsToField,
|
|
1293
|
+
mutable: true,
|
|
1294
|
+
enumerable: true,
|
|
1295
|
+
serializable: true
|
|
1296
|
+
},
|
|
1297
|
+
collection: {
|
|
1298
|
+
get: getCollectionField,
|
|
1299
|
+
set: setCollectionField,
|
|
1300
|
+
mutable: true,
|
|
1301
|
+
enumerable: true,
|
|
1302
|
+
serializable: true
|
|
1303
|
+
},
|
|
1304
|
+
derived: {
|
|
1305
|
+
get: getDerivedField,
|
|
1306
|
+
set: setDerivedField,
|
|
1307
|
+
mutable: true,
|
|
1308
|
+
enumerable: true,
|
|
1309
|
+
serializable: false
|
|
1310
|
+
},
|
|
1311
|
+
field: {
|
|
1312
|
+
get: getGenericField,
|
|
1313
|
+
set: setGenericField,
|
|
1314
|
+
mutable: true,
|
|
1315
|
+
enumerable: true,
|
|
1316
|
+
serializable: true
|
|
1317
|
+
},
|
|
1318
|
+
hasMany: {
|
|
1319
|
+
get: getHasManyField,
|
|
1320
|
+
set: setHasManyField,
|
|
1321
|
+
mutable: true,
|
|
1322
|
+
enumerable: true,
|
|
1323
|
+
serializable: true
|
|
1324
|
+
},
|
|
1325
|
+
object: {
|
|
1326
|
+
get: getObjectField,
|
|
1327
|
+
set: setObjectField,
|
|
1328
|
+
mutable: true,
|
|
1329
|
+
enumerable: true,
|
|
1330
|
+
serializable: true
|
|
1331
|
+
},
|
|
1332
|
+
resource: {
|
|
1333
|
+
get: getResourceField,
|
|
1334
|
+
set: setResourceField,
|
|
1335
|
+
mutable: true,
|
|
1336
|
+
enumerable: true,
|
|
1337
|
+
serializable: true
|
|
1338
|
+
},
|
|
1339
|
+
'schema-array': {
|
|
1340
|
+
get: getArrayField,
|
|
1341
|
+
set: setSchemaArrayField,
|
|
1342
|
+
mutable: true,
|
|
1343
|
+
enumerable: true,
|
|
1344
|
+
serializable: true
|
|
1345
|
+
},
|
|
1346
|
+
'schema-object': {
|
|
1347
|
+
get: getSchemaObjectField,
|
|
1348
|
+
set: setSchemaObjectField,
|
|
1349
|
+
mutable: true,
|
|
1350
|
+
enumerable: true,
|
|
1351
|
+
serializable: true
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
716
1354
|
const IgnoredGlobalFields = new Set(['length', 'nodeType', 'then', 'setInterval', 'document', STRUCTURED]);
|
|
717
1355
|
const symbolList = [Destroy, RecordStore, Identifier, Editable, Parent, Checkout, Legacy, EmbeddedPath, EmbeddedField];
|
|
718
1356
|
const RecordSymbols = new Set(symbolList);
|
|
@@ -737,27 +1375,41 @@ const Editables = new WeakMap();
|
|
|
737
1375
|
*/
|
|
738
1376
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
739
1377
|
class ReactiveResource {
|
|
740
|
-
constructor(
|
|
741
|
-
|
|
742
|
-
|
|
1378
|
+
constructor(context) {
|
|
1379
|
+
const {
|
|
1380
|
+
store
|
|
1381
|
+
} = context;
|
|
1382
|
+
const identifier = context.resourceKey;
|
|
1383
|
+
const embeddedField = context.field;
|
|
1384
|
+
const embeddedPath = context.path;
|
|
1385
|
+
const isEmbedded = context.field !== null;
|
|
743
1386
|
this[RecordStore] = store;
|
|
744
1387
|
if (isEmbedded) {
|
|
745
1388
|
this[Parent] = identifier;
|
|
746
1389
|
} else {
|
|
747
1390
|
this[Identifier] = identifier;
|
|
748
1391
|
}
|
|
749
|
-
const IS_EDITABLE = this[Editable] =
|
|
750
|
-
this[Legacy] = Mode[Legacy] ?? false;
|
|
1392
|
+
const IS_EDITABLE = this[Editable] = context.editable ?? false;
|
|
751
1393
|
const schema = store.schema;
|
|
752
|
-
|
|
753
|
-
const
|
|
1394
|
+
this[Legacy] = context.legacy ?? false;
|
|
1395
|
+
const objectType = isEmbedded ? context.value : identifier.type;
|
|
1396
|
+
const ResourceSchema = schema.resource(isEmbedded ? {
|
|
1397
|
+
type: objectType
|
|
1398
|
+
} : identifier);
|
|
1399
|
+
const identityField = ResourceSchema.identity;
|
|
754
1400
|
const BoundFns = new Map();
|
|
755
1401
|
|
|
756
1402
|
// prettier-ignore
|
|
757
|
-
const extensions = !
|
|
1403
|
+
const extensions = !context.legacy ? null : isEmbedded ? schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(embeddedField, objectType) : schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(identifier);
|
|
758
1404
|
this[EmbeddedField] = embeddedField;
|
|
759
1405
|
this[EmbeddedPath] = embeddedPath;
|
|
760
|
-
const fields = isEmbedded ? schema.fields(
|
|
1406
|
+
const fields = isEmbedded ? schema.fields({
|
|
1407
|
+
type: objectType
|
|
1408
|
+
}) : schema.fields(identifier);
|
|
1409
|
+
const method = typeof schema.cacheFields === 'function' ? 'cacheFields' : 'fields';
|
|
1410
|
+
const cacheFields = isEmbedded ? schema[method]({
|
|
1411
|
+
type: objectType
|
|
1412
|
+
}) : schema[method](identifier);
|
|
761
1413
|
const signals = withSignalStore(this);
|
|
762
1414
|
const proxy = new Proxy(this, {
|
|
763
1415
|
ownKeys() {
|
|
@@ -934,74 +1586,64 @@ class ReactiveResource {
|
|
|
934
1586
|
throw new Error(`Alias fields cannot alias '@id' '@local' '@hash' or 'derived' fields`);
|
|
935
1587
|
}
|
|
936
1588
|
})(maybeField.kind !== 'alias' || !['@id', '@local', '@hash', 'derived'].includes(maybeField.options.kind)) : {};
|
|
1589
|
+
/**
|
|
1590
|
+
* Prop Array is the path from a resource to the field including
|
|
1591
|
+
* intermediate "links" on arrays,objects,schema-arrays and schema-objects.
|
|
1592
|
+
*
|
|
1593
|
+
* E.g. in the following
|
|
1594
|
+
*
|
|
1595
|
+
* ```
|
|
1596
|
+
* const user = {
|
|
1597
|
+
* addresses: [{
|
|
1598
|
+
* street: 'Sunset Blvd',
|
|
1599
|
+
* zip: 90210
|
|
1600
|
+
* }]
|
|
1601
|
+
* }
|
|
1602
|
+
* ```
|
|
1603
|
+
*
|
|
1604
|
+
* The propArray for "street" is ['addresses', 0, 'street']
|
|
1605
|
+
*
|
|
1606
|
+
* Prop Array follows the `cache` path to the value, not the ui path.
|
|
1607
|
+
* Thus, if `addresses` has a sourceKey of `user_addresses` and
|
|
1608
|
+
* `zip` has a sourceKey of `zip_code` then the propArray for "zip" is
|
|
1609
|
+
* ['user_addresses', 0, 'zip_code']
|
|
1610
|
+
*/
|
|
937
1611
|
const propArray = isEmbedded ? embeddedPath.slice() : [];
|
|
938
1612
|
// we use the field.name instead of prop here because we want to use the cache-path not
|
|
939
1613
|
// the record path.
|
|
940
|
-
|
|
941
|
-
// propArray.
|
|
942
|
-
|
|
1614
|
+
// SAFETY: we lie as string here because if we were to get null
|
|
1615
|
+
// we would be in a field kind that won't use the propArray below.
|
|
1616
|
+
const fieldCacheKey = getFieldCacheKey(field);
|
|
1617
|
+
propArray.push(fieldCacheKey);
|
|
943
1618
|
switch (field.kind) {
|
|
944
1619
|
case '@id':
|
|
945
|
-
entangleSignal(signals, receiver, '@identity', null);
|
|
946
|
-
return identifier.id;
|
|
947
1620
|
case '@hash':
|
|
948
|
-
// TODO pass actual cache value not {}
|
|
949
|
-
return schema.hashFn(field)({}, field.options ?? null, field.name ?? null);
|
|
950
1621
|
case '@local':
|
|
951
|
-
|
|
952
|
-
return computeLocal(receiver, field, prop);
|
|
953
|
-
}
|
|
1622
|
+
case 'derived':
|
|
954
1623
|
case 'field':
|
|
955
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
956
|
-
return computeField(schema, cache, target, identifier, field, propArray, IS_EDITABLE);
|
|
957
1624
|
case 'attribute':
|
|
958
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
959
|
-
return computeAttribute(cache, identifier, prop, IS_EDITABLE);
|
|
960
|
-
case 'resource':
|
|
961
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
962
|
-
return computeResource(store, cache, target, identifier, field, prop, IS_EDITABLE);
|
|
963
|
-
case 'derived':
|
|
964
|
-
return computeDerivation(schema, receiver, identifier, field, prop);
|
|
965
1625
|
case 'schema-array':
|
|
966
1626
|
case 'array':
|
|
967
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
968
|
-
return computeArray(store, schema, cache, target, identifier, field, propArray, Mode[Editable], Mode[Legacy]);
|
|
969
|
-
case 'object':
|
|
970
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
971
|
-
return computeObject(schema, cache, target, identifier, field, propArray, Mode[Editable], Mode[Legacy]);
|
|
972
1627
|
case 'schema-object':
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
return computeSchemaObject(store, cache, target, identifier, field, propArray, Mode[Legacy], Mode[Editable]);
|
|
1628
|
+
case 'object':
|
|
1629
|
+
case 'resource':
|
|
976
1630
|
case 'belongsTo':
|
|
977
|
-
if (field.options.linksMode) {
|
|
978
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
979
|
-
const rawValue = IS_EDITABLE ? cache.getRelationship(identifier, field.name) : cache.getRemoteRelationship(identifier, field.name);
|
|
980
|
-
|
|
981
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
982
|
-
return rawValue.data ? store.peekRecord(rawValue.data) : null;
|
|
983
|
-
}
|
|
984
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
985
|
-
if (!test) {
|
|
986
|
-
throw new Error(`Can only use belongsTo fields when the resource is in legacy mode`);
|
|
987
|
-
}
|
|
988
|
-
})(Mode[Legacy]) : {};
|
|
989
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
990
|
-
return schema._kind('@legacy', 'belongsTo').get(store, receiver, identifier, field);
|
|
991
1631
|
case 'hasMany':
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1632
|
+
case 'collection':
|
|
1633
|
+
return DefaultMode[field.kind].get({
|
|
1634
|
+
store,
|
|
1635
|
+
resourceKey: identifier,
|
|
1636
|
+
modeName: context.modeName,
|
|
1637
|
+
legacy: context.legacy,
|
|
1638
|
+
editable: context.editable,
|
|
1639
|
+
path: propArray,
|
|
1640
|
+
field: field,
|
|
1641
|
+
record: receiver,
|
|
1642
|
+
signals,
|
|
1643
|
+
value: null
|
|
1644
|
+
});
|
|
1003
1645
|
default:
|
|
1004
|
-
|
|
1646
|
+
assertNeverField(identifier, field, propArray);
|
|
1005
1647
|
}
|
|
1006
1648
|
},
|
|
1007
1649
|
set(target, prop, value, receiver) {
|
|
@@ -1028,203 +1670,64 @@ class ReactiveResource {
|
|
|
1028
1670
|
throw new Error(`Alias fields cannot alias '@id' '@local' '@hash' or 'derived' fields`);
|
|
1029
1671
|
}
|
|
1030
1672
|
})(maybeField.kind !== 'alias' || !['@id', '@local', '@hash', 'derived'].includes(maybeField.options.kind)) : {};
|
|
1673
|
+
/**
|
|
1674
|
+
* Prop Array is the path from a resource to the field including
|
|
1675
|
+
* intermediate "links" on arrays,objects,schema-arrays and schema-objects.
|
|
1676
|
+
*
|
|
1677
|
+
* E.g. in the following
|
|
1678
|
+
*
|
|
1679
|
+
* ```
|
|
1680
|
+
* const user = {
|
|
1681
|
+
* addresses: [{
|
|
1682
|
+
* street: 'Sunset Blvd',
|
|
1683
|
+
* zip: 90210
|
|
1684
|
+
* }]
|
|
1685
|
+
* }
|
|
1686
|
+
* ```
|
|
1687
|
+
*
|
|
1688
|
+
* The propArray for "street" is ['addresses', 0, 'street']
|
|
1689
|
+
*
|
|
1690
|
+
* Prop Array follows the `cache` path to the value, not the ui path.
|
|
1691
|
+
* Thus, if `addresses` has a sourceKey of `user_addresses` and
|
|
1692
|
+
* `zip` has a sourceKey of `zip_code` then the propArray for "zip" is
|
|
1693
|
+
* ['user_addresses', 0, 'zip_code']
|
|
1694
|
+
*/
|
|
1031
1695
|
const propArray = isEmbedded ? embeddedPath.slice() : [];
|
|
1032
1696
|
// we use the field.name instead of prop here because we want to use the cache-path not
|
|
1033
1697
|
// the record path.
|
|
1034
|
-
|
|
1035
|
-
// propArray.
|
|
1036
|
-
|
|
1698
|
+
// SAFETY: we lie as string here because if we were to get null
|
|
1699
|
+
// we would be in a field kind that won't use the propArray below.
|
|
1700
|
+
const fieldCacheKey = getFieldCacheKey(field);
|
|
1701
|
+
propArray.push(fieldCacheKey);
|
|
1037
1702
|
switch (field.kind) {
|
|
1038
1703
|
case '@id':
|
|
1039
|
-
|
|
1040
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1041
|
-
if (!test) {
|
|
1042
|
-
throw new Error(`Expected to receive a string id`);
|
|
1043
|
-
}
|
|
1044
|
-
})(typeof value === 'string' && value.length) : {};
|
|
1045
|
-
const normalizedId = String(value);
|
|
1046
|
-
const didChange = normalizedId !== identifier.id;
|
|
1047
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1048
|
-
if (!test) {
|
|
1049
|
-
throw new Error(`Cannot set ${identifier.type} record's id to ${normalizedId}, because id is already ${identifier.id}`);
|
|
1050
|
-
}
|
|
1051
|
-
})(!didChange || identifier.id === null) : {};
|
|
1052
|
-
if (normalizedId !== null && didChange) {
|
|
1053
|
-
store._instanceCache.setRecordId(identifier, normalizedId);
|
|
1054
|
-
store.notifications.notify(identifier, 'identity');
|
|
1055
|
-
}
|
|
1056
|
-
return true;
|
|
1057
|
-
}
|
|
1704
|
+
case '@hash':
|
|
1058
1705
|
case '@local':
|
|
1059
|
-
{
|
|
1060
|
-
const signal = getOrCreateInternalSignal(signals, receiver, prop, field.options?.defaultValue ?? null);
|
|
1061
|
-
if (signal.value !== value) {
|
|
1062
|
-
signal.value = value;
|
|
1063
|
-
notifyInternalSignal(signal);
|
|
1064
|
-
}
|
|
1065
|
-
return true;
|
|
1066
|
-
}
|
|
1067
1706
|
case 'field':
|
|
1068
|
-
{
|
|
1069
|
-
if (!field.type) {
|
|
1070
|
-
cache.setAttr(identifier, propArray, value);
|
|
1071
|
-
return true;
|
|
1072
|
-
}
|
|
1073
|
-
const transform = schema.transformation(field);
|
|
1074
|
-
const rawValue = transform.serialize(value, field.options ?? null, target);
|
|
1075
|
-
cache.setAttr(identifier, propArray, rawValue);
|
|
1076
|
-
return true;
|
|
1077
|
-
}
|
|
1078
1707
|
case 'attribute':
|
|
1079
|
-
|
|
1080
|
-
cache.setAttr(identifier, propArray, value);
|
|
1081
|
-
return true;
|
|
1082
|
-
}
|
|
1708
|
+
case 'derived':
|
|
1083
1709
|
case 'array':
|
|
1084
|
-
{
|
|
1085
|
-
if (!field.type) {
|
|
1086
|
-
cache.setAttr(identifier, propArray, value?.slice());
|
|
1087
|
-
const peeked = peekManagedArray(self, field);
|
|
1088
|
-
if (peeked) {
|
|
1089
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1090
|
-
if (!test) {
|
|
1091
|
-
throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
|
|
1092
|
-
}
|
|
1093
|
-
})(ARRAY_SIGNAL in peeked) : {};
|
|
1094
|
-
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
1095
|
-
arrSignal.isStale = true;
|
|
1096
|
-
}
|
|
1097
|
-
if (!Array.isArray(value)) {
|
|
1098
|
-
ManagedArrayMap.delete(target);
|
|
1099
|
-
}
|
|
1100
|
-
return true;
|
|
1101
|
-
}
|
|
1102
|
-
const transform = schema.transformation(field);
|
|
1103
|
-
const rawValue = value.map(item => transform.serialize(item, field.options ?? null, target));
|
|
1104
|
-
cache.setAttr(identifier, propArray, rawValue);
|
|
1105
|
-
const peeked = peekManagedArray(self, field);
|
|
1106
|
-
if (peeked) {
|
|
1107
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1108
|
-
if (!test) {
|
|
1109
|
-
throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
|
|
1110
|
-
}
|
|
1111
|
-
})(ARRAY_SIGNAL in peeked) : {};
|
|
1112
|
-
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
1113
|
-
arrSignal.isStale = true;
|
|
1114
|
-
}
|
|
1115
|
-
return true;
|
|
1116
|
-
}
|
|
1117
1710
|
case 'schema-array':
|
|
1118
|
-
{
|
|
1119
|
-
const arrayValue = value?.slice();
|
|
1120
|
-
if (!Array.isArray(arrayValue)) {
|
|
1121
|
-
ManagedArrayMap.delete(target);
|
|
1122
|
-
}
|
|
1123
|
-
cache.setAttr(identifier, propArray, arrayValue);
|
|
1124
|
-
const peeked = peekManagedArray(self, field);
|
|
1125
|
-
if (peeked) {
|
|
1126
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1127
|
-
if (!test) {
|
|
1128
|
-
throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
|
|
1129
|
-
}
|
|
1130
|
-
})(ARRAY_SIGNAL in peeked) : {};
|
|
1131
|
-
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
1132
|
-
arrSignal.isStale = true;
|
|
1133
|
-
}
|
|
1134
|
-
if (!Array.isArray(value)) {
|
|
1135
|
-
ManagedArrayMap.delete(target);
|
|
1136
|
-
}
|
|
1137
|
-
return true;
|
|
1138
|
-
}
|
|
1139
|
-
case 'object':
|
|
1140
|
-
{
|
|
1141
|
-
if (!field.type) {
|
|
1142
|
-
let newValue = value;
|
|
1143
|
-
if (value !== null) {
|
|
1144
|
-
newValue = {
|
|
1145
|
-
...value
|
|
1146
|
-
};
|
|
1147
|
-
} else {
|
|
1148
|
-
ManagedObjectMap.delete(target);
|
|
1149
|
-
}
|
|
1150
|
-
cache.setAttr(identifier, propArray, newValue);
|
|
1151
|
-
const peeked = peekManagedObject(self, field);
|
|
1152
|
-
if (peeked) {
|
|
1153
|
-
const objSignal = peeked[OBJECT_SIGNAL];
|
|
1154
|
-
objSignal.isStale = true;
|
|
1155
|
-
}
|
|
1156
|
-
return true;
|
|
1157
|
-
}
|
|
1158
|
-
const transform = schema.transformation(field);
|
|
1159
|
-
const rawValue = transform.serialize({
|
|
1160
|
-
...value
|
|
1161
|
-
}, field.options ?? null, target);
|
|
1162
|
-
cache.setAttr(identifier, propArray, rawValue);
|
|
1163
|
-
const peeked = peekManagedObject(self, field);
|
|
1164
|
-
if (peeked) {
|
|
1165
|
-
const objSignal = peeked[OBJECT_SIGNAL];
|
|
1166
|
-
objSignal.isStale = true;
|
|
1167
|
-
}
|
|
1168
|
-
return true;
|
|
1169
|
-
}
|
|
1170
1711
|
case 'schema-object':
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
if (value !== null) {
|
|
1174
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1175
|
-
if (!test) {
|
|
1176
|
-
throw new Error(`Expected value to be an object`);
|
|
1177
|
-
}
|
|
1178
|
-
})(typeof value === 'object') : {};
|
|
1179
|
-
newValue = {
|
|
1180
|
-
...value
|
|
1181
|
-
};
|
|
1182
|
-
const schemaFields = schema.fields({
|
|
1183
|
-
type: field.type
|
|
1184
|
-
});
|
|
1185
|
-
for (const key of Object.keys(newValue)) {
|
|
1186
|
-
if (!schemaFields.has(key)) {
|
|
1187
|
-
throw new Error(`Field ${key} does not exist on schema object ${field.type}`);
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
} else {
|
|
1191
|
-
ManagedObjectMap.delete(target);
|
|
1192
|
-
}
|
|
1193
|
-
cache.setAttr(identifier, propArray, newValue);
|
|
1194
|
-
// const peeked = peekManagedObject(self, field);
|
|
1195
|
-
// if (peeked) {
|
|
1196
|
-
// const objSignal = peeked[OBJECT_SIGNAL];
|
|
1197
|
-
// objSignal.isStale = true;
|
|
1198
|
-
// }
|
|
1199
|
-
return true;
|
|
1200
|
-
}
|
|
1201
|
-
case 'derived':
|
|
1202
|
-
{
|
|
1203
|
-
throw new Error(`Cannot set ${String(prop)} on ${identifier.type} because it is derived`);
|
|
1204
|
-
}
|
|
1712
|
+
case 'object':
|
|
1713
|
+
case 'resource':
|
|
1205
1714
|
case 'belongsTo':
|
|
1206
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1207
|
-
if (!test) {
|
|
1208
|
-
throw new Error(`Can only use belongsTo fields when the resource is in legacy mode`);
|
|
1209
|
-
}
|
|
1210
|
-
})(Mode[Legacy]) : {};
|
|
1211
|
-
schema._kind('@legacy', 'belongsTo').set(store, receiver, identifier, field, value);
|
|
1212
|
-
return true;
|
|
1213
1715
|
case 'hasMany':
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1716
|
+
case 'collection':
|
|
1717
|
+
return DefaultMode[field.kind].set({
|
|
1718
|
+
store,
|
|
1719
|
+
resourceKey: identifier,
|
|
1720
|
+
modeName: context.modeName,
|
|
1721
|
+
legacy: context.legacy,
|
|
1722
|
+
editable: context.editable,
|
|
1723
|
+
path: propArray,
|
|
1724
|
+
field: field,
|
|
1725
|
+
record: receiver,
|
|
1726
|
+
signals,
|
|
1727
|
+
value
|
|
1728
|
+
});
|
|
1226
1729
|
default:
|
|
1227
|
-
|
|
1730
|
+
return assertNeverField(identifier, field, propArray);
|
|
1228
1731
|
}
|
|
1229
1732
|
}
|
|
1230
1733
|
});
|
|
@@ -1255,25 +1758,25 @@ class ReactiveResource {
|
|
|
1255
1758
|
// TODO we should likely handle this notification here
|
|
1256
1759
|
// also we should add a LOGGING flag
|
|
1257
1760
|
// eslint-disable-next-line no-console
|
|
1258
|
-
console.warn(`Notification unhandled for ${key.join(',')} on ${identifier.type}`,
|
|
1761
|
+
console.warn(`Notification unhandled for ${key.join(',')} on ${identifier.type}`, proxy);
|
|
1259
1762
|
return;
|
|
1260
1763
|
}
|
|
1261
1764
|
|
|
1262
1765
|
// TODO we should add a LOGGING flag
|
|
1263
|
-
// console.log(`Deep notification skipped for ${key.join('.')} on ${identifier.type}`,
|
|
1766
|
+
// console.log(`Deep notification skipped for ${key.join('.')} on ${identifier.type}`, proxy);
|
|
1264
1767
|
// deep notify the key path
|
|
1265
1768
|
} else {
|
|
1266
1769
|
if (isEmbedded) return; // base paths never apply to embedded records
|
|
1267
1770
|
|
|
1268
1771
|
// TODO determine what LOGGING flag to wrap this in if any
|
|
1269
|
-
// console.log(`Notification for ${key} on ${identifier.type}`,
|
|
1772
|
+
// console.log(`Notification for ${key} on ${identifier.type}`, proxy);
|
|
1270
1773
|
const signal = signals.get(key);
|
|
1271
1774
|
if (signal) {
|
|
1272
1775
|
notifyInternalSignal(signal);
|
|
1273
1776
|
}
|
|
1274
|
-
const field =
|
|
1777
|
+
const field = cacheFields.get(key);
|
|
1275
1778
|
if (field?.kind === 'array' || field?.kind === 'schema-array') {
|
|
1276
|
-
const peeked =
|
|
1779
|
+
const peeked = signal?.value;
|
|
1277
1780
|
if (peeked) {
|
|
1278
1781
|
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1279
1782
|
if (!test) {
|
|
@@ -1285,7 +1788,7 @@ class ReactiveResource {
|
|
|
1285
1788
|
}
|
|
1286
1789
|
}
|
|
1287
1790
|
if (field?.kind === 'object') {
|
|
1288
|
-
const peeked = peekManagedObject(
|
|
1791
|
+
const peeked = peekManagedObject(proxy, field);
|
|
1289
1792
|
if (peeked) {
|
|
1290
1793
|
const objSignal = peeked[OBJECT_SIGNAL];
|
|
1291
1794
|
notifyInternalSignal(objSignal);
|
|
@@ -1299,15 +1802,15 @@ class ReactiveResource {
|
|
|
1299
1802
|
if (Array.isArray(key)) ;else {
|
|
1300
1803
|
if (isEmbedded) return; // base paths never apply to embedded records
|
|
1301
1804
|
|
|
1302
|
-
const field =
|
|
1805
|
+
const field = cacheFields.get(key);
|
|
1303
1806
|
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1304
1807
|
if (!test) {
|
|
1305
|
-
throw new Error(`Expected
|
|
1808
|
+
throw new Error(`Expected relationship ${key} to be the name of a field`);
|
|
1306
1809
|
}
|
|
1307
1810
|
})(field) : {};
|
|
1308
1811
|
if (field.kind === 'belongsTo') {
|
|
1309
1812
|
// TODO determine what LOGGING flag to wrap this in if any
|
|
1310
|
-
// console.log(`Notification for ${key} on ${identifier.type}`,
|
|
1813
|
+
// console.log(`Notification for ${key} on ${identifier.type}`, proxy);
|
|
1311
1814
|
const signal = signals.get(key);
|
|
1312
1815
|
if (signal) {
|
|
1313
1816
|
notifyInternalSignal(signal);
|
|
@@ -1315,9 +1818,12 @@ class ReactiveResource {
|
|
|
1315
1818
|
// FIXME
|
|
1316
1819
|
} else if (field.kind === 'resource') ;else if (field.kind === 'hasMany') {
|
|
1317
1820
|
if (field.options.linksMode) {
|
|
1318
|
-
const
|
|
1319
|
-
if (
|
|
1320
|
-
|
|
1821
|
+
const signal = signals.get(key);
|
|
1822
|
+
if (signal) {
|
|
1823
|
+
const peeked = signal.value;
|
|
1824
|
+
if (peeked) {
|
|
1825
|
+
notifyInternalSignal(peeked[ARRAY_SIGNAL]);
|
|
1826
|
+
}
|
|
1321
1827
|
}
|
|
1322
1828
|
return;
|
|
1323
1829
|
}
|
|
@@ -1325,7 +1831,7 @@ class ReactiveResource {
|
|
|
1325
1831
|
if (!test) {
|
|
1326
1832
|
throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
|
|
1327
1833
|
}
|
|
1328
|
-
})(
|
|
1834
|
+
})(context.legacy) : {};
|
|
1329
1835
|
if (schema._kind('@legacy', 'hasMany').notify(store, proxy, identifier, field)) {
|
|
1330
1836
|
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1331
1837
|
if (!test) {
|
|
@@ -1381,10 +1887,17 @@ function _CHECKOUT(record) {
|
|
|
1381
1887
|
if (isEmbedded) {
|
|
1382
1888
|
throw new Error(`Cannot checkout an embedded record (yet)`);
|
|
1383
1889
|
}
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1890
|
+
const legacy = record[Legacy];
|
|
1891
|
+
const editableRecord = new ReactiveResource({
|
|
1892
|
+
store: record[RecordStore],
|
|
1893
|
+
resourceKey: record[Identifier],
|
|
1894
|
+
modeName: legacy ? 'legacy' : 'polaris',
|
|
1895
|
+
legacy: legacy,
|
|
1896
|
+
editable: true,
|
|
1897
|
+
path: null,
|
|
1898
|
+
field: null,
|
|
1899
|
+
value: null
|
|
1900
|
+
});
|
|
1388
1901
|
setRecordIdentifier(editableRecord, recordIdentifierFor(record));
|
|
1389
1902
|
return Promise.resolve(editableRecord);
|
|
1390
1903
|
}
|
|
@@ -1396,6 +1909,17 @@ function _DESTROY(record) {
|
|
|
1396
1909
|
record.isDestroyed = true;
|
|
1397
1910
|
}
|
|
1398
1911
|
record[RecordStore].notifications.unsubscribe(record.___notifications);
|
|
1912
|
+
|
|
1913
|
+
// FIXME we need a way to also unsubscribe all SchemaObjects when the primary
|
|
1914
|
+
// resource is destroyed.
|
|
1915
|
+
}
|
|
1916
|
+
function assertNeverField(identifier, field, path) {
|
|
1917
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1918
|
+
{
|
|
1919
|
+
throw new Error(`Cannot use unknown field kind ${field.kind} on <${identifier.type}>.${Array.isArray(path) ? path.join('.') : path}`);
|
|
1920
|
+
}
|
|
1921
|
+
})() : {};
|
|
1922
|
+
return false;
|
|
1399
1923
|
}
|
|
1400
1924
|
function instantiateRecord(store, identifier, createArgs) {
|
|
1401
1925
|
const schema = store.schema;
|
|
@@ -1405,11 +1929,17 @@ function instantiateRecord(store, identifier, createArgs) {
|
|
|
1405
1929
|
throw new Error(`Expected a resource schema`);
|
|
1406
1930
|
}
|
|
1407
1931
|
})(isResourceSchema(resourceSchema)) : {};
|
|
1408
|
-
const
|
|
1409
|
-
const
|
|
1410
|
-
const record = new ReactiveResource(
|
|
1411
|
-
|
|
1412
|
-
|
|
1932
|
+
const legacy = resourceSchema?.legacy ?? false;
|
|
1933
|
+
const editable = legacy || store.cache.isNew(identifier);
|
|
1934
|
+
const record = new ReactiveResource({
|
|
1935
|
+
store,
|
|
1936
|
+
resourceKey: identifier,
|
|
1937
|
+
modeName: legacy ? 'legacy' : 'polaris',
|
|
1938
|
+
legacy: legacy,
|
|
1939
|
+
editable: editable,
|
|
1940
|
+
path: null,
|
|
1941
|
+
field: null,
|
|
1942
|
+
value: null
|
|
1413
1943
|
});
|
|
1414
1944
|
if (createArgs) {
|
|
1415
1945
|
Object.assign(record, createArgs);
|
|
@@ -1546,7 +2076,7 @@ function getExt(extCache, type, extName) {
|
|
|
1546
2076
|
function hasObjectSchema(field) {
|
|
1547
2077
|
return 'kind' in field && (field.kind === 'schema-array' || field.kind === 'schema-object');
|
|
1548
2078
|
}
|
|
1549
|
-
function processExtensions(schema, field, scenario) {
|
|
2079
|
+
function processExtensions(schema, field, scenario, resolvedType) {
|
|
1550
2080
|
// if we're looking up extensions for a resource, there is no
|
|
1551
2081
|
// merging required so if we have no objectExtensions
|
|
1552
2082
|
// we are done.
|
|
@@ -1573,12 +2103,16 @@ function processExtensions(schema, field, scenario) {
|
|
|
1573
2103
|
if (!hasObjectSchema(field)) {
|
|
1574
2104
|
return null;
|
|
1575
2105
|
}
|
|
1576
|
-
return schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(
|
|
2106
|
+
return schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resolvedType ? {
|
|
2107
|
+
type: resolvedType
|
|
2108
|
+
} : field);
|
|
1577
2109
|
}
|
|
1578
2110
|
|
|
1579
2111
|
// if we have made it here, we have extensions, lets check if there's
|
|
1580
2112
|
// a cached version we can use
|
|
1581
|
-
const baseExtensions = scenario === 'resource' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(field) : scenario === 'object' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(
|
|
2113
|
+
const baseExtensions = scenario === 'resource' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(field) : scenario === 'object' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resolvedType ? {
|
|
2114
|
+
type: resolvedType
|
|
2115
|
+
} : field) : null;
|
|
1582
2116
|
if (!baseExtensions && extensions.length === 1) {
|
|
1583
2117
|
const value = getExt(extCache, type, extensions[0]);
|
|
1584
2118
|
fieldCache[type].set(field, value);
|
|
@@ -1836,16 +2370,21 @@ class SchemaService {
|
|
|
1836
2370
|
relationships[field.name] = field;
|
|
1837
2371
|
}
|
|
1838
2372
|
}
|
|
2373
|
+
const cacheFields = null;
|
|
1839
2374
|
const traits = new Set(isResourceSchema(schema) ? schema.traits : []);
|
|
1840
2375
|
const finalized = traits.size === 0;
|
|
1841
2376
|
const internalSchema = {
|
|
1842
2377
|
original: schema,
|
|
1843
2378
|
finalized,
|
|
1844
2379
|
fields,
|
|
2380
|
+
cacheFields,
|
|
1845
2381
|
relationships,
|
|
1846
2382
|
attributes,
|
|
1847
2383
|
traits
|
|
1848
2384
|
};
|
|
2385
|
+
if (traits.size === 0) {
|
|
2386
|
+
internalSchema.cacheFields = getCacheFields(internalSchema);
|
|
2387
|
+
}
|
|
1849
2388
|
this._schemas.set(schema.type, internalSchema);
|
|
1850
2389
|
}
|
|
1851
2390
|
|
|
@@ -1902,13 +2441,13 @@ class SchemaService {
|
|
|
1902
2441
|
}
|
|
1903
2442
|
CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resource) {
|
|
1904
2443
|
const schema = this.resource(resource);
|
|
1905
|
-
return processExtensions(this, schema, 'resource');
|
|
2444
|
+
return processExtensions(this, schema, 'resource', null);
|
|
1906
2445
|
}
|
|
1907
|
-
CAUTION_MEGA_DANGER_ZONE_objectExtensions(field) {
|
|
1908
|
-
return processExtensions(this, field, 'object');
|
|
2446
|
+
CAUTION_MEGA_DANGER_ZONE_objectExtensions(field, resolvedType) {
|
|
2447
|
+
return processExtensions(this, field, 'object', resolvedType);
|
|
1909
2448
|
}
|
|
1910
2449
|
CAUTION_MEGA_DANGER_ZONE_arrayExtensions(field) {
|
|
1911
|
-
return processExtensions(this, field, 'array');
|
|
2450
|
+
return processExtensions(this, field, 'array', null);
|
|
1912
2451
|
}
|
|
1913
2452
|
CAUTION_MEGA_DANGER_ZONE_hasExtension(ext) {
|
|
1914
2453
|
return this._extensions[ext.kind].has(ext.name);
|
|
@@ -1976,6 +2515,20 @@ class SchemaService {
|
|
|
1976
2515
|
}
|
|
1977
2516
|
return schema.fields;
|
|
1978
2517
|
}
|
|
2518
|
+
cacheFields({
|
|
2519
|
+
type
|
|
2520
|
+
}) {
|
|
2521
|
+
const schema = this._schemas.get(type);
|
|
2522
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
2523
|
+
if (!test) {
|
|
2524
|
+
throw new Error(`No schema defined for ${type}`);
|
|
2525
|
+
}
|
|
2526
|
+
})(schema) : {};
|
|
2527
|
+
if (!schema.finalized) {
|
|
2528
|
+
finalizeResource(this, schema);
|
|
2529
|
+
}
|
|
2530
|
+
return schema.cacheFields;
|
|
2531
|
+
}
|
|
1979
2532
|
hasResource(resource) {
|
|
1980
2533
|
return this._schemas.has(resource.type);
|
|
1981
2534
|
}
|
|
@@ -2064,8 +2617,27 @@ function finalizeResource(schema, resource) {
|
|
|
2064
2617
|
}
|
|
2065
2618
|
mergeMap(fields, resource.fields);
|
|
2066
2619
|
resource.fields = fields;
|
|
2620
|
+
resource.cacheFields = getCacheFields(resource);
|
|
2067
2621
|
resource.finalized = true;
|
|
2068
2622
|
}
|
|
2623
|
+
function getCacheFields(resource) {
|
|
2624
|
+
const {
|
|
2625
|
+
fields
|
|
2626
|
+
} = resource;
|
|
2627
|
+
const cacheFields = new Map();
|
|
2628
|
+
for (const [key, value] of fields) {
|
|
2629
|
+
if (isNonIdentityCacheableField(value)) {
|
|
2630
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
2631
|
+
if (!test) {
|
|
2632
|
+
throw new Error(`The sourceKey '${value.sourceKey}' for the field '${key}' on ${resource.original.type} is invalid because it matches the name of an existing field`);
|
|
2633
|
+
}
|
|
2634
|
+
})(!value.sourceKey || value.sourceKey === key || !fields.has(value.sourceKey)) : {};
|
|
2635
|
+
const cacheKey = getFieldCacheKeyStrict(value);
|
|
2636
|
+
cacheFields.set(cacheKey, value);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
return cacheFields;
|
|
2640
|
+
}
|
|
2069
2641
|
function walkTrait(schema, trait, fields, seen, type, debugPath) {
|
|
2070
2642
|
if (seen.has(trait)) {
|
|
2071
2643
|
// if the trait is in the current path, we throw a cycle error in dev.
|